diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 2239408..f10541d 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -63,6 +63,10 @@ jobs: uses: goreleaser/goreleaser-action@v6 with: install-only: true + - name: Set Join Key in system.yaml + uses: mikefarah/yq@master + with: + cmd: yq -i '.shared += {"security": {"joinKey": "${{ secrets.ARTIFACTORY_JOIN_KEY }}"}}' ${{ github.workspace }}/scripts/system.yaml - name: Create Artifactory data directories and copy data env: ARTIFACTORY_LICENSE: ${{ secrets.ARTIFACTORY_LICENSE }} @@ -72,6 +76,11 @@ jobs: echo $ARTIFACTORY_LICENSE > ${{ runner.temp }}/artifactory/extra_conf/artifactory.lic cp ${{ github.workspace }}/scripts/system.yaml ${{ runner.temp }}/artifactory/var/etc/system.yaml sudo chown -R 1030:1030 ${{ runner.temp }}/artifactory/var + mkdir -p ${{ runner.temp }}/artifactory-2/extra_conf + mkdir -p ${{ runner.temp }}/artifactory-2/var/etc + echo $ARTIFACTORY_LICENSE > ${{ runner.temp }}/artifactory-2/extra_conf/artifactory.lic + cp ${{ github.workspace }}/scripts/system.yaml ${{ runner.temp }}/artifactory-2/var/etc/system.yaml + sudo chown -R 1030:1030 ${{ runner.temp }}/artifactory-2/var - name: Run Artifactory container id: run_artifactory_container run: | @@ -86,6 +95,12 @@ jobs: -v ${{ runner.temp }}/artifactory/var:/var/opt/jfrog/artifactory \ -p 8081:8081 -p 8082:8082 \ releases-docker.jfrog.io/jfrog/artifactory-pro:${ARTIFACTORY_VERSION} + echo "Start up Artifactory 2 container" + docker run -i --name artifactory-2 -d --rm \ + -v ${{ runner.temp }}/artifactory-2/extra_conf:/artifactory_extra_conf \ + -v ${{ runner.temp }}/artifactory-2/var:/var/opt/jfrog/artifactory \ + -p 9081:8081 -p 9082:8082 \ + releases-docker.jfrog.io/jfrog/artifactory-pro:${ARTIFACTORY_VERSION} echo "Set localhost to a container IP address, since we run docker inside of docker" export LOCALHOST=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.Gateway}}{{end}}' artifactory) export JFROG_URL="http://${LOCALHOST}:8082" @@ -95,6 +110,14 @@ jobs: printf '.' sleep 5 done + export LOCALHOST_2=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.Gateway}}{{end}}' artifactory-2) + export JFROG_URL_2="http://${LOCALHOST-2}:9082" + echo "ARTIFACTORY_URL_2=$JFROG_URL_2" >> "$GITHUB_ENV" + echo "Waiting for Artifactory 2 services to start at ${JFROG_URL_2}" + until $(curl -sf -o /dev/null -m 5 ${JFROG_URL_2}/artifactory/api/system/ping/); do + printf '.' + sleep 5 + done echo "Waiting for Artifactory UI to start" until $(curl -sf -o /dev/null -m 5 ${JFROG_URL}/ui/login/); do printf '.' @@ -123,6 +146,7 @@ jobs: env: JFROG_LICENSE_BUCKET_URL: ${{ secrets.JFROG_LICENSE_BUCKET_URL }} JFROG_LICENSE_BUCKET_KEY: ${{ secrets.JFROG_LICENSE_BUCKET_KEY }} + ARTIFACTORY_JOIN_KEY: ${{ secrets.ARTIFACTORY_JOIN_KEY }} run: make acceptance - name: Install provider run: | diff --git a/.gitignore b/.gitignore index c8d3dbb..59ac5d6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,5 @@ scripts/artifactory_license*.json coverage.txt .scannerwork *.code-workspace -scripts/artifactory/ +scripts/artifactory*/ *.hcl \ No newline at end of file diff --git a/pkg/missioncontrol/provider.go b/pkg/missioncontrol/provider.go index 34794b0..1cf973a 100644 --- a/pkg/missioncontrol/provider.go +++ b/pkg/missioncontrol/provider.go @@ -147,6 +147,7 @@ func (p *MissionControlProvider) DataSources(ctx context.Context) []func() datas func (p *MissionControlProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewLicenseBucketResource, + NewJPDResource, } } diff --git a/pkg/missioncontrol/resource_jpd.go b/pkg/missioncontrol/resource_jpd.go new file mode 100644 index 0000000..f05494e --- /dev/null +++ b/pkg/missioncontrol/resource_jpd.go @@ -0,0 +1,629 @@ +package missioncontrol + +import ( + "context" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validator_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "github.com/samber/lo" +) + +const ( + jpdsEndpoint = "mc/api/v1/jpds" + jpdEndpoint = "mc/api/v1/jpds/{id}" +) + +var _ resource.Resource = &jpdResource{} + +type jpdResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +func NewJPDResource() resource.Resource { + return &jpdResource{ + TypeName: "missioncontrol_jpd", + } +} + +func (r *jpdResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +func (r *jpdResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Description: "A unique logical name for this Platform Deployment", + }, + "url": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validator_string.IsURLHttpOrHttps(), + stringvalidator.RegexMatches(regexp.MustCompile(`^.+/$`), "must end in '/'"), + }, + Description: "The Platform deployment URL: http://:/; for example: http://myplatformserver:8082/. Note: For legacy instances, version 6.x and lower, the URL should contain the instance root context: http://://; for example http://myv6server:8081/artifactory/. URL must ends with trailing slash.", + }, + "token": schema.StringAttribute{ + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.ConflictsWith(path.MatchRoot("username"), path.MatchRoot("password")), + }, + Description: "JPD join key", + }, + "username": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.AlsoRequires(path.MatchRoot("password")), + stringvalidator.ConflictsWith(path.MatchRoot("url")), + }, + Description: "Admin username for legacy JPD (Artifactory 6.x).", + }, + "password": schema.StringAttribute{ + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.AlsoRequires(path.MatchRoot("username")), + stringvalidator.ConflictsWith(path.MatchRoot("url")), + }, + Description: "Admin password for legacy JPD (Artifactory 6.x).", + }, + "location": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "city_name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "country_code": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(2, 2), + }, + Description: "2 letters ISO-3166-2 country code", + }, + "latitude": schema.Float64Attribute{ + Required: true, + }, + "longitude": schema.Float64Attribute{ + Required: true, + }, + }, + Required: true, + Description: "The geographical location of the Platform Deployment to be displayed on a global Platform Deployment view", + }, + "tags": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "Add labels to be applied for filtering Platform Deployments according to categories for example, location, dedicated centers - dev, testing, production", + }, + "base_url": schema.StringAttribute{ + Computed: true, + }, + "licenses": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "expired": schema.BoolAttribute{ + Computed: true, + }, + "license_hash": schema.StringAttribute{ + Computed: true, + }, + "licensed_to": schema.StringAttribute{ + Computed: true, + }, + "type": schema.StringAttribute{ + Computed: true, + }, + "valid_through": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + }, + "services": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Computed: true, + }, + "status": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "code": schema.StringAttribute{ + Computed: true, + }, + }, + Computed: true, + }, + }, + }, + Computed: true, + }, + "status": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "code": schema.StringAttribute{ + Computed: true, + }, + "message": schema.StringAttribute{ + Computed: true, + }, + "warnings": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + }, + Computed: true, + }, + "local": schema.BoolAttribute{ + Computed: true, + }, + "is_cold_storage": schema.BoolAttribute{ + Computed: true, + }, + "cold_storage_jpd": schema.StringAttribute{ + Computed: true, + }, + }, + MarkdownDescription: "Provides a [JFrog Platform Deployment](https://jfrog.com/help/r/jfrog-platform-administration-documentation/manage-platform-deployments) resource to manage JPD.\n~>Supported on the Self-Hosted platform, with an Enterprise X or Enterprise+ license.", + } +} + +type jpdResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + URL types.String `tfsdk:"url"` + BaseURL types.String `tfsdk:"base_url"` + Token types.String `tfsdk:"token"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + Location types.Object `tfsdk:"location"` + Services types.Set `tfsdk:"services"` + Licenses types.Set `tfsdk:"licenses"` + Tags types.Set `tfsdk:"tags"` + Local types.Bool `tfsdk:"local"` + Status types.Object `tfsdk:"status"` + IsColdStorage types.Bool `tfsdk:"is_cold_storage"` + ColdStorageJPD types.String `tfsdk:"cold_storage_jpd"` +} + +var licenseAttrTypes = map[string]attr.Type{ + "expired": types.BoolType, + "license_hash": types.StringType, + "licensed_to": types.StringType, + "type": types.StringType, + "valid_through": types.StringType, +} + +var licenseElemType = types.ObjectType{ + AttrTypes: licenseAttrTypes, +} + +var serviceAttrTypes = map[string]attr.Type{ + "status": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "code": types.StringType, + }, + }, + "type": types.StringType, +} + +var serviceElemType = types.ObjectType{ + AttrTypes: serviceAttrTypes, +} + +func (r *jpdResourceModel) fromAPIModel(ctx context.Context, apiModel *jpdGetResponseAPIModel) (ds diag.Diagnostics) { + r.ID = types.StringValue(apiModel.ID) + r.Name = types.StringValue(apiModel.Name) + r.URL = types.StringValue(apiModel.URL) + r.BaseURL = types.StringValue(apiModel.BaseURL) + + location, d := types.ObjectValue( + map[string]attr.Type{ + "city_name": types.StringType, + "country_code": types.StringType, + "latitude": types.Float64Type, + "longitude": types.Float64Type, + }, + map[string]attr.Value{ + "city_name": types.StringValue(apiModel.Location.CityName), + "country_code": types.StringValue(apiModel.Location.CountryCode), + "latitude": types.Float64Value(apiModel.Location.Latitude), + "longitude": types.Float64Value(apiModel.Location.Longitude), + }, + ) + if d.HasError() { + ds.Append(d...) + } + r.Location = location + + licenses := lo.Map( + apiModel.Licenses, + func(license jpdLicenseAPIModel, _ int) attr.Value { + l, d := types.ObjectValue( + licenseAttrTypes, + map[string]attr.Value{ + "expired": types.BoolValue(license.Expired), + "license_hash": types.StringValue(license.LicenseHash), + "licensed_to": types.StringValue(license.LicensedTo), + "type": types.StringValue(license.Type), + "valid_through": types.StringValue(license.ValidThrough), + }, + ) + if d.HasError() { + ds.Append(d...) + } + + return l + }, + ) + licensesSet, d := types.SetValue(licenseElemType, licenses) + if d.HasError() { + ds.Append(d...) + } + r.Licenses = licensesSet + + services := lo.Map( + apiModel.Services, + func(service jpdServiceAPIModel, _ int) attr.Value { + status, d := types.ObjectValue( + map[string]attr.Type{ + "code": types.StringType, + }, + map[string]attr.Value{ + "code": types.StringValue(service.Status.Code), + }, + ) + if d.HasError() { + ds.Append(d...) + } + + s, d := types.ObjectValue( + serviceAttrTypes, + map[string]attr.Value{ + "status": status, + "type": types.StringValue(service.Type), + }, + ) + if d.HasError() { + ds.Append(d...) + } + + return s + }, + ) + servicesSet, d := types.SetValue(serviceElemType, services) + if d.HasError() { + ds.Append(d...) + } + r.Services = servicesSet + + warnings, d := types.SetValueFrom(ctx, types.StringType, apiModel.Status.Warnings) + if d.HasError() { + ds.Append(d...) + } + status, d := types.ObjectValue( + map[string]attr.Type{ + "code": types.StringType, + "message": types.StringType, + "warnings": types.SetType{ElemType: types.StringType}, + }, + map[string]attr.Value{ + "code": types.StringValue(apiModel.Status.Code), + "message": types.StringValue(apiModel.Status.Message), + "warnings": warnings, + }, + ) + if d.HasError() { + ds.Append(d...) + } + r.Status = status + + tags, d := types.SetValueFrom(ctx, types.StringType, apiModel.Tags) + if d.HasError() { + ds.Append(d...) + } + r.Tags = tags + r.Local = types.BoolValue(apiModel.Local) + r.IsColdStorage = types.BoolValue(apiModel.IsColdStorage) + + r.ColdStorageJPD = types.StringNull() + if apiModel.IsColdStorage { + r.ColdStorageJPD = types.StringValue(apiModel.ColdStorageJPD) + } + + return +} + +func (r jpdResourceModel) toAPIModel(ctx context.Context, apiModel *jpdPostRequestAPIModel, artifactoryVersion string) diag.Diagnostics { + ds := diag.Diagnostics{} + + var tags []string + ds.Append(r.Tags.ElementsAs(ctx, &tags, false)...) + + locationAttrs := r.Location.Attributes() + *apiModel = jpdPostRequestAPIModel{ + Name: r.Name.ValueString(), + URL: r.URL.ValueString(), + Location: jpdLocationAPIModel{ + CityName: locationAttrs["city_name"].(types.String).ValueString(), + CountryCode: locationAttrs["country_code"].(types.String).ValueString(), + Latitude: locationAttrs["latitude"].(types.Float64).ValueFloat64(), + Longitude: locationAttrs["longitude"].(types.Float64).ValueFloat64(), + }, + Tags: tags, + } + + notLegacy, err := util.CheckVersion(artifactoryVersion, "7.0.0") + if err != nil { + ds.AddError("faild to check version", err.Error()) + } + + if notLegacy { + apiModel.Token = r.Token.ValueString() + } else { + apiModel.Username = r.Username.ValueString() + apiModel.Password = r.Password.ValueString() + } + + return ds +} + +type jpdPostRequestAPIModel struct { + Name string `json:"name"` + URL string `json:"url"` + Token string `json:"token,omitempty"` + Username string `json:"username,omitempty"` // legacy (Artifactory 6.x only) + Password string `json:"password,omitempty"` // legacy (Artifactory 6.x only) + Location jpdLocationAPIModel `json:"location"` + Tags []string `json:"tags"` +} + +type jpdLocationAPIModel struct { + CityName string `json:"city_name"` + CountryCode string `json:"country_code"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type jpdGetResponseAPIModel struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + BaseURL string `json:"base_url"` + Location jpdLocationAPIModel `json:"location"` + Local bool `json:"local"` + Licenses []jpdLicenseAPIModel `json:"licenses"` + Services []jpdServiceAPIModel `json:"services"` + Status jpdStatusAPIModel `json:"status"` + Tags []string `json:"tags"` + IsColdStorage bool `json:"is_cold_storage"` + ColdStorageJPD string `json:"cold_storage_jpd"` +} + +type jpdLicenseAPIModel struct { + Expired bool `json:"expired"` + LicenseHash string `json:"license_hash"` + LicensedTo string `json:"licensed_to"` + Type string `json:"type"` + ValidThrough string `json:"valid_through"` +} + +type jpdServiceAPIModel struct { + Status jpdServiceStatusAPIModel `json:"status"` + Type string `json:"type"` +} + +type jpdServiceStatusAPIModel struct { + Code string `json:"code"` +} + +type jpdStatusAPIModel struct { + Code string `json:"code"` + Message string `json:"message"` + Warnings []string `json:"warnings"` +} + +func (r *jpdResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + r.ProviderData = req.ProviderData.(util.ProviderMetadata) +} + +func (r *jpdResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan jpdResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var jpd jpdPostRequestAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &jpd, r.ProviderData.ArtifactoryVersion)...) + if resp.Diagnostics.HasError() { + return + } + + var result jpdGetResponseAPIModel + response, err := r.ProviderData.Client.R(). + SetBody(jpd). + SetResult(&result). + Post(jpdsEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + // Convert from the API data model to the Terraform data model + // and refresh any attribute values. + resp.Diagnostics.Append(plan.fromAPIModel(ctx, &result)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *jpdResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state jpdResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var jpd jpdGetResponseAPIModel + response, err := r.ProviderData.Client.R(). + SetPathParam("id", state.ID.ValueString()). + SetResult(&jpd). + Get(jpdEndpoint) + + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, response.String()) + return + } + + // Convert from the API data model to the Terraform data model + // and refresh any attribute values. + resp.Diagnostics.Append(state.fromAPIModel(ctx, &jpd)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *jpdResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan jpdResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var state jpdResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var jpd jpdPostRequestAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &jpd, r.ProviderData.ArtifactoryVersion)...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.ProviderData.Client.R(). + SetPathParam("id", state.ID.ValueString()). + SetBody(jpd). + Put(jpdEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + var result jpdGetResponseAPIModel + response, err = r.ProviderData.Client.R(). + SetPathParam("id", state.ID.ValueString()). + SetResult(&result). + Get(jpdEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + // Convert from the API data model to the Terraform data model + // and refresh any attribute values. + resp.Diagnostics.Append(plan.fromAPIModel(ctx, &result)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *jpdResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state jpdResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.ProviderData.Client.R(). + SetPathParam("id", state.ID.ValueString()). + Delete(jpdEndpoint) + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, response.String()) + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *jpdResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/pkg/missioncontrol/resource_jpd_test.go b/pkg/missioncontrol/resource_jpd_test.go new file mode 100644 index 0000000..69e85e7 --- /dev/null +++ b/pkg/missioncontrol/resource_jpd_test.go @@ -0,0 +1,143 @@ +package missioncontrol_test + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +// To make tests work runs ./scripts/run-artifactory-2.sh which will export env var `ARTIFACTORY_URL_2` +func skipTest() (bool, string) { + if len(os.Getenv("ARTIFACTORY_URL_2")) > 0 && len(os.Getenv("ARTIFACTORY_JOIN_KEY")) > 0 { + return false, "Env var `ARTIFACTORY_URL_2` and `ARTIFACTORY_JOIN_KEY` are set. Executing test." + } + + return true, "Env var `ARTIFACTORY_URL_2` or `ARTIFACTORY_JOIN_KEY` are not set. Skipping test." +} + +func TestAccJpd_full(t *testing.T) { + if skip, reason := skipTest(); skip { + t.Skipf(reason) + } + + _, fqrn, resourceName := testutil.MkNames("test-jpd", "missioncontrol_jpd") + + temp := ` + resource "missioncontrol_jpd" "{{ .name }}" { + name = "{{ .name }}" + url = "http://host.docker.internal:9082/" + token = "{{ .token }}" + + location = { + city_name = "San Francisco" + country_code = "US" + latitude = 37.7749 + longitude = 122.4194 + } + + tags = [ + "prod", + "dev", + ] + }` + + // Get the join key from the second Artifactory instance web UI: https://jfrog.com/help/r/jfrog-platform-administration-documentation/view-the-join-key + // then set the value to env var ARTIFACTORY_2_JOIN_KEY + testData := map[string]string{ + "name": resourceName, + "token": os.Getenv("ARTIFACTORY_JOIN_KEY"), + } + + config := util.ExecuteTemplate(resourceName, temp, testData) + + updatedTemp := ` + resource "missioncontrol_jpd" "{{ .name }}" { + name = "{{ .name }}" + url = "http://host.docker.internal:9082/" + token = "{{ .token }}" + + location = { + city_name = "New York" + country_code = "US" + latitude = 40.7128 + longitude = 74.006 + } + + tags = [ + "dev", + ] + }` + updatedConfig := util.ExecuteTemplate(resourceName, updatedTemp, testData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", testData["name"]), + resource.TestCheckResourceAttr(fqrn, "url", "http://host.docker.internal:9082/"), + resource.TestCheckResourceAttr(fqrn, "location.city_name", "San Francisco"), + resource.TestCheckResourceAttr(fqrn, "location.country_code", "US"), + resource.TestCheckResourceAttr(fqrn, "location.latitude", "37.7749"), + resource.TestCheckResourceAttr(fqrn, "location.longitude", "122.4194"), + resource.TestCheckResourceAttr(fqrn, "tags.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "tags.*", "prod"), + resource.TestCheckTypeSetElemAttr(fqrn, "tags.*", "dev"), + resource.TestCheckResourceAttrSet(fqrn, "id"), + resource.TestCheckResourceAttrSet(fqrn, "base_url"), + resource.TestCheckResourceAttr(fqrn, "status.code", "ONLINE"), + resource.TestCheckResourceAttrSet(fqrn, "status.message"), + resource.TestCheckResourceAttr(fqrn, "status.warnings.#", "0"), + resource.TestCheckResourceAttr(fqrn, "local", "false"), + resource.TestCheckResourceAttr(fqrn, "services.#", "1"), + resource.TestCheckResourceAttr(fqrn, "services.0.type", "ARTIFACTORY"), + resource.TestCheckResourceAttr(fqrn, "services.0.status.code", "ONLINE"), + resource.TestCheckResourceAttr(fqrn, "licenses.#", "0"), + resource.TestCheckResourceAttr(fqrn, "is_cold_storage", "false"), + resource.TestCheckNoResourceAttr(fqrn, "cold_storage_jpd"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", testData["name"]), + resource.TestCheckResourceAttr(fqrn, "url", "http://host.docker.internal:9082/"), + resource.TestCheckResourceAttr(fqrn, "location.city_name", "New York"), + resource.TestCheckResourceAttr(fqrn, "location.country_code", "US"), + resource.TestCheckResourceAttr(fqrn, "location.latitude", "40.7128"), + resource.TestCheckResourceAttr(fqrn, "location.longitude", "74.006"), + resource.TestCheckResourceAttr(fqrn, "tags.#", "1"), + resource.TestCheckTypeSetElemAttr(fqrn, "tags.*", "dev"), + resource.TestCheckResourceAttrSet(fqrn, "id"), + resource.TestCheckResourceAttrSet(fqrn, "base_url"), + resource.TestCheckResourceAttr(fqrn, "status.code", "ONLINE"), + resource.TestCheckResourceAttrSet(fqrn, "status.message"), + resource.TestCheckResourceAttr(fqrn, "status.warnings.#", "0"), + resource.TestCheckResourceAttr(fqrn, "local", "false"), + resource.TestCheckResourceAttr(fqrn, "services.#", "1"), + resource.TestCheckResourceAttr(fqrn, "services.0.type", "ARTIFACTORY"), + resource.TestCheckResourceAttr(fqrn, "services.0.status.code", "ONLINE"), + resource.TestCheckResourceAttr(fqrn, "licenses.#", "1"), + resource.TestCheckResourceAttrSet(fqrn, "licenses.0.type"), + resource.TestCheckResourceAttrSet(fqrn, "licenses.0.expired"), + resource.TestCheckResourceAttrSet(fqrn, "licenses.0.license_hash"), + resource.TestCheckResourceAttrSet(fqrn, "licenses.0.licensed_to"), + resource.TestCheckResourceAttrSet(fqrn, "licenses.0.valid_through"), + resource.TestCheckResourceAttr(fqrn, "is_cold_storage", "false"), + resource.TestCheckNoResourceAttr(fqrn, "cold_storage_jpd"), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"token", "username", "password"}, + }, + }, + }) +} diff --git a/scripts/run-artifactory-2.sh b/scripts/run-artifactory-2.sh new file mode 100755 index 0000000..fdcf857 --- /dev/null +++ b/scripts/run-artifactory-2.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )" +source "${SCRIPT_DIR}/get-access-key.sh" +source "${SCRIPT_DIR}/wait-for-rt.sh" + +export ARTIFACTORY_VERSION=${ARTIFACTORY_VERSION:-7.84.15} +echo "ARTIFACTORY_VERSION=${ARTIFACTORY_VERSION}" > /dev/stderr + +set -euf + +sudo rm -rf ${SCRIPT_DIR}/artifactory-2/ + +mkdir -p ${SCRIPT_DIR}/artifactory-2/extra_conf +mkdir -p ${SCRIPT_DIR}/artifactory-2/var/etc/access + +cp ${SCRIPT_DIR}/artifactory.lic ${SCRIPT_DIR}/artifactory-2/extra_conf +cp ${SCRIPT_DIR}/system.yaml ${SCRIPT_DIR}/artifactory-2/var/etc/ +cp ${SCRIPT_DIR}/access.config.patch.yml ${SCRIPT_DIR}/artifactory-2/var/etc/access + +if [[ -z "${ARTIFACTORY_JOIN_KEY}" ]]; then + yq -i '.shared += {"security": {"joinKey": "$ARTIFACTORY_JOIN_KEY"}}' ${SCRIPT_DIR}/artifactory-2/var/etc/system.yaml +fi + +docker run -i --name artifactory-2 -d --rm \ + -e JF_FRONTEND_FEATURETOGGLER_ACCESSINTEGRATION=true \ + -v ${SCRIPT_DIR}/artifactory-2/extra_conf:/artifactory_extra_conf \ + -v ${SCRIPT_DIR}/artifactory-2/var:/var/opt/jfrog/artifactory \ + -p 9081:8081 -p 9082:8082 \ + releases-docker.jfrog.io/jfrog/artifactory-pro:${ARTIFACTORY_VERSION} + +export ARTIFACTORY_URL_2=http://localhost:9081 +export ARTIFACTORY_UI_URL_2=http://localhost:9082 + +# Wait for Artifactory to start +waitForArtifactory "${ARTIFACTORY_URL_2}" "${ARTIFACTORY_UI_URL_2}" diff --git a/scripts/run-artifactory.sh b/scripts/run-artifactory.sh index 62be6b9..aeb952b 100755 --- a/scripts/run-artifactory.sh +++ b/scripts/run-artifactory.sh @@ -18,6 +18,10 @@ cp ${SCRIPT_DIR}/artifactory.lic ${SCRIPT_DIR}/artifactory/extra_conf cp ${SCRIPT_DIR}/system.yaml ${SCRIPT_DIR}/artifactory/var/etc/ cp ${SCRIPT_DIR}/access.config.patch.yml ${SCRIPT_DIR}/artifactory/var/etc/access +if [[ -z "${ARTIFACTORY_JOIN_KEY}" ]]; then + yq -i '.shared += {"security": {"joinKey": "$ARTIFACTORY_JOIN_KEY"}}' ${SCRIPT_DIR}/artifactory/var/etc/system.yaml +fi + docker run -i --name artifactory -d --rm \ -e JF_FRONTEND_FEATURETOGGLER_ACCESSINTEGRATION=true \ -v ${SCRIPT_DIR}/artifactory/extra_conf:/artifactory_extra_conf \