diff --git a/examples/resources/humanitec_registry/import.sh b/examples/resources/humanitec_registry/import.sh new file mode 100755 index 0000000..3603195 --- /dev/null +++ b/examples/resources/humanitec_registry/import.sh @@ -0,0 +1 @@ +terraform import humanitec_registry.example registry_id diff --git a/examples/resources/humanitec_registry/resource.tf b/examples/resources/humanitec_registry/resource.tf new file mode 100644 index 0000000..c9518af --- /dev/null +++ b/examples/resources/humanitec_registry/resource.tf @@ -0,0 +1,12 @@ +resource "humanitec_registry" "example" { + id = "example-registry" + registry = "registry.example.com" + type = "secret_ref" + enable_ci = true + secrets = { + cluster-a = { + namespace = "example-namespace" + secret = "path/to/secret" + } + } +} diff --git a/go.mod b/go.mod index 07c2716..434386a 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/terraform-plugin-go v0.19.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 - github.com/humanitec/humanitec-go-autogen v0.0.0-20231011074422-e4592591bb03 + github.com/humanitec/humanitec-go-autogen v0.0.0-20231213120624-80322ffb0f43 github.com/stretchr/testify v1.8.4 ) @@ -53,7 +53,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/uuid v1.3.1 // indirect + github.com/google/uuid v1.4.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect @@ -85,7 +85,7 @@ require ( github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/labstack/echo/v4 v4.11.2 // indirect + github.com/labstack/echo/v4 v4.11.3 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect @@ -101,6 +101,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oapi-codegen/runtime v1.1.0 // indirect github.com/oklog/run v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -135,5 +136,6 @@ require ( google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 473fa6f..31f0569 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -189,6 +191,8 @@ github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/humanitec/humanitec-go-autogen v0.0.0-20231011074422-e4592591bb03 h1:EN2UREH8bXYtRaPEq93tkN0TTkowWFKOXdXGQ6YgkLs= github.com/humanitec/humanitec-go-autogen v0.0.0-20231011074422-e4592591bb03/go.mod h1:RZj+jfsRy1Z6sGPlfEYPFXl+hLW9nGo/4VQzm35yqs8= +github.com/humanitec/humanitec-go-autogen v0.0.0-20231213120624-80322ffb0f43 h1:8KuHxEUwcJGsK1UGFek/3daXOkGgpz1qDguP2GIhHjQ= +github.com/humanitec/humanitec-go-autogen v0.0.0-20231213120624-80322ffb0f43/go.mod h1:QmeSmyBNgMIbbJ6b4MefN0aEOUpMXqZzIuvlMqEYKus= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= @@ -238,6 +242,7 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE= github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= @@ -281,6 +286,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oapi-codegen/runtime v1.1.0 h1:rJpoNUawn5XTvekgfkvSZr0RqEnoYpFkyvrzfWeFKWM= +github.com/oapi-codegen/runtime v1.1.0/go.mod h1:BeSfBkWWWnAnGdyS+S/GnlbmHKzf8/hwkvelJZDeKA8= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f255c79..bf282a5 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -163,6 +163,7 @@ func (p *HumanitecProvider) Resources(ctx context.Context) []func() resource.Res NewResourceEnvironmentTypeUser(true), NewResourceEnvironmentTypeUser(false), NewResourcePipeline, + NewResourceRegistry, NewResourceResourceDriver, NewResourceRule, NewResourceSecretStore, diff --git a/internal/provider/resource_artefact_version.go b/internal/provider/resource_artefact_version.go index c58fcd3..ee33af0 100644 --- a/internal/provider/resource_artefact_version.go +++ b/internal/provider/resource_artefact_version.go @@ -4,11 +4,13 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "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/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/humanitec/humanitec-go-autogen" @@ -63,6 +65,9 @@ func (r *ResourceArtefactVersion) Schema(ctx context.Context, req resource.Schem "type": schema.StringAttribute{ MarkdownDescription: "The Artefact Version type.", Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("container"), + }, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, @@ -128,15 +133,17 @@ func setOptionalStringValue(target types.String, source string) types.String { return types.StringValue(source) } -func parseArtefactVersionResponse(res *client.ArtefactVersionResponse, artefactRes *client.ArtefactResponse, data *ArtefactVersionModel) { +func parseArtefactVersionResponse(res client.ContainerArtefactVersion, data *ArtefactVersionModel) { data.ID = types.StringValue(res.Id) data.Name = types.StringValue(res.Name) - data.Type = types.StringValue(artefactRes.Type) + data.Type = types.StringValue("container") data.Commit = setOptionalStringValue(data.Commit, res.Commit) data.Digest = setOptionalStringValue(data.Digest, res.Digest) data.Ref = setOptionalStringValue(data.Ref, res.Ref) - data.Version = setOptionalStringValue(data.Version, res.Version) + if res.Version != nil { + data.Version = setOptionalStringValue(data.Version, *res.Version) + } } func (r *ResourceArtefactVersion) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -149,53 +156,46 @@ func (r *ResourceArtefactVersion) Create(ctx context.Context, req resource.Creat return } - httpResp, err := r.client.PostOrgsOrgIdArtefactVersionsWithResponse(ctx, r.orgId, &client.PostOrgsOrgIdArtefactVersionsParams{}, client.PostOrgsOrgIdArtefactVersionsJSONRequestBody{ + artefactContainerRequest := client.CreateContainerArtefactVersion{ Commit: data.Commit.ValueStringPointer(), Digest: data.Digest.ValueStringPointer(), Name: data.Name.ValueString(), Ref: data.Ref.ValueStringPointer(), Type: data.Type.ValueString(), Version: data.Version.ValueStringPointer(), - }) - if err != nil { - resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create artefact version, got error: %s", err)) - return } - if httpResp.StatusCode() != 200 { - resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to create artefact version, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body)) - return + artefactRequest := client.CreateArtefactVersionJSONRequestBody{ + Type: "container", } - - artefactHttpResp, err := r.client.GetOrgsOrgIdArtefactsWithResponse(ctx, r.orgId, &client.GetOrgsOrgIdArtefactsParams{ - Name: &httpResp.JSON200.Name, - }) + err := artefactRequest.MergeCreateContainerArtefactVersion(artefactContainerRequest) if err != nil { - resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to fetch created artefact, got error: %s", err)) - return + resp.Diagnostics.AddError( + "Unexpected error", + fmt.Sprintf("Failed to create a request for container artefact creation: %v", err), + ) } - if httpResp.StatusCode() != 200 { - resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to fetch created artefact, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body)) + createArtefactVersionResp, err := r.client.CreateArtefactVersionWithResponse(ctx, r.orgId, &client.CreateArtefactVersionParams{}, artefactRequest) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create artefact version, got error: %s", err)) return } - artefacts := *artefactHttpResp.JSON200 - artefactId := httpResp.JSON200.ArtefactId - var artefact *client.ArtefactResponse - for _, a := range artefacts { - if a.Id == artefactId { - artefact = &a - break - } + if createArtefactVersionResp.StatusCode() != 200 { + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to create artefact version, unexpected status code: %d, body: %s", createArtefactVersionResp.StatusCode(), createArtefactVersionResp.Body)) + return } - if artefact == nil { - resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to fetch created artefact, id (%s) not found in response, %v", artefactId, artefacts)) - return + artefactContainerResponse, err := createArtefactVersionResp.JSON200.AsContainerArtefactVersion() + if err != nil { + resp.Diagnostics.AddError( + "Unexpected error", + fmt.Sprintf("Failed to read container artefact creation response: %v", err), + ) } - parseArtefactVersionResponse(httpResp.JSON200, artefact, data) + parseArtefactVersionResponse(artefactContainerResponse, data) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -211,41 +211,26 @@ func (r *ResourceArtefactVersion) Read(ctx context.Context, req resource.ReadReq return } - httpResp, err := r.client.GetOrgsOrgIdArtefactVersionsArtefactVersionIdWithResponse(ctx, r.orgId, data.ID.ValueString()) + getArtefactVersionResp, err := r.client.GetArtefactVersionWithResponse(ctx, r.orgId, data.ID.ValueString()) if err != nil { resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to read ArtefactVersion, got error: %s", err)) return } - if httpResp.StatusCode() != 200 { - resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to read ArtefactVersion, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body)) + if getArtefactVersionResp.StatusCode() != 200 { + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to read ArtefactVersion, unexpected status code: %d, body: %s", getArtefactVersionResp.StatusCode(), getArtefactVersionResp.Body)) return } - artefactHttpResp, err := r.client.GetOrgsOrgIdArtefactsWithResponse(ctx, r.orgId, &client.GetOrgsOrgIdArtefactsParams{ - Name: &httpResp.JSON200.Name, - }) + artefactContainerResponse, err := getArtefactVersionResp.JSON200.AsContainerArtefactVersion() if err != nil { - resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to read artefact, got error: %s", err)) - return - } - - if httpResp.StatusCode() != 200 { - resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to read artefact, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body)) - return - } - - artefactId := httpResp.JSON200.ArtefactId - artefact, found := findInSlicePtr(artefactHttpResp.JSON200, func(a client.ArtefactResponse) bool { - return a.Id == artefactId - }) - - if !found { - resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to read artefact, id (%s) not found in response, %+v", artefactId, artefactHttpResp.JSON200)) - return + resp.Diagnostics.AddError( + "Unexpected error", + fmt.Sprintf("Failed to read container artefact creation response: %v", err), + ) } - parseArtefactVersionResponse(httpResp.JSON200, &artefact, data) + parseArtefactVersionResponse(artefactContainerResponse, data) // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/provider/resource_pipeline.go b/internal/provider/resource_pipeline.go index 2117b11..4b6f1eb 100644 --- a/internal/provider/resource_pipeline.go +++ b/internal/provider/resource_pipeline.go @@ -122,7 +122,7 @@ func (r *ResourcePipeline) Create(ctx context.Context, req resource.CreateReques appID := data.AppID.ValueString() definition := data.Definition.ValueString() - var pipeline *client.PipelineResponse + var pipeline *client.Pipeline createPipelineResp, err := r.client.CreatePipelineWithBodyWithResponse(ctx, r.orgID, appID, "application/x-yaml", strings.NewReader(definition)) if err != nil { resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create pipeline, got error: %s", err)) @@ -165,7 +165,7 @@ func (r *ResourcePipeline) Read(ctx context.Context, req resource.ReadRequest, r appID := data.AppID.ValueString() id := data.ID.ValueString() - var pipeline *client.PipelineResponse + var pipeline *client.Pipeline getPipelineResp, err := r.client.GetPipelineWithResponse(ctx, r.orgID, appID, id, &client.GetPipelineParams{}) if err != nil { resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to get pipeline, got error: %s", err)) @@ -190,7 +190,7 @@ func (r *ResourcePipeline) Read(ctx context.Context, req resource.ReadRequest, r } contentType := "application/x.humanitec-pipelines-v1.0+yaml" - getPipelineDefinitionResp, err := r.client.GetPipelineSchemaWithResponse(ctx, r.orgID, appID, id, &client.GetPipelineSchemaParams{ + getPipelineDefinitionResp, err := r.client.GetPipelineDefinitionWithResponse(ctx, r.orgID, appID, id, &client.GetPipelineDefinitionParams{ Accept: &contentType, }) if err != nil { @@ -224,7 +224,7 @@ func (r *ResourcePipeline) Update(ctx context.Context, req resource.UpdateReques id := state.ID.ValueString() definition := data.Definition.ValueString() - var pipeline *client.PipelineResponse + var pipeline *client.Pipeline updatePipelineResp, err := r.client.UpdatePipelineWithBodyWithResponse(ctx, r.orgID, appID, id, &client.UpdatePipelineParams{}, "application/x-yaml", strings.NewReader(definition)) if err != nil { resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update pipeline, got error: %s", err)) @@ -318,7 +318,7 @@ func (r *ResourcePipeline) ImportState(ctx context.Context, req resource.ImportS } } -func parsePipelineResponse(ctx context.Context, res *client.PipelineResponse, data *PipelineModel) diag.Diagnostics { +func parsePipelineResponse(ctx context.Context, res *client.Pipeline, data *PipelineModel) diag.Diagnostics { totalDiags := diag.Diagnostics{} data.AppID = types.StringValue(res.AppId) diff --git a/internal/provider/resource_registry.go b/internal/provider/resource_registry.go new file mode 100644 index 0000000..9c36773 --- /dev/null +++ b/internal/provider/resource_registry.go @@ -0,0 +1,388 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/humanitec/humanitec-go-autogen" + "github.com/humanitec/humanitec-go-autogen/client" +) + +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &ResourceRegistry{} +var _ resource.ResourceWithImportState = &ResourceRegistry{} + +func NewResourceRegistry() resource.Resource { + return &ResourceRegistry{} +} + +// ResourceRule defines the resource implementation. +type ResourceRegistry struct { + client *humanitec.Client + orgID string +} + +func (r *ResourceRegistry) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_registry" +} + +func (r *ResourceRegistry) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Container Registries store and manage container images ready for use when they are needed in a deployment.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Registry ID, unique within the Organization.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile("^[a-z0-9][a-z0-9-]+[a-z0-9]$"), "must follow standard Humanitec id pattern"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "registry": schema.StringAttribute{ + MarkdownDescription: "Registry name, usually in a \"{domain}\" or \"{domain}/{project}\" format.", + Required: true, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "Registry type, describes the registry authentication method, and defines the schema for the credentials.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("basic", "google_gcr", "amazon_ecr", "secret_ref"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + + "enable_ci": schema.BoolAttribute{ + MarkdownDescription: "Indicates if registry secrets and credentials should be exposed to CI agents.", + Optional: true, + }, + "creds": schema.ObjectAttribute{ + MarkdownDescription: "AccountCreds represents an account credentials (either, username- or token-based).", + Optional: true, + AttributeTypes: map[string]attr.Type{ + "password": types.StringType, + "username": types.StringType, + }, + Sensitive: true, + }, + "secrets": schema.MapNestedAttribute{ + MarkdownDescription: "ClusterSecretsMap stores a list of Kuberenetes secret references for the target deployment clusters.", + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "namespace": schema.StringAttribute{ + Required: true, + }, + "secret": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + } +} + +func (r *ResourceRegistry) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + resdata, ok := req.ProviderData.(*HumanitecData) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = resdata.Client + r.orgID = resdata.OrgID +} + +type RegistryModel struct { + ID types.String `tfsdk:"id"` + Registry types.String `tfsdk:"registry"` + Type types.String `tfsdk:"type"` + EnableCI types.Bool `tfsdk:"enable_ci"` + Creds types.Object `tfsdk:"creds"` + Secrets *map[string]SecretsModel `tfsdk:"secrets"` +} + +type SecretsModel struct { + Namespace types.String `tfsdk:"namespace"` + Secret types.String `tfsdk:"secret"` +} + +func (r *ResourceRegistry) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *RegistryModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + request, diags := parseRegistryModel(ctx, data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var registry *client.RegistryResponse + createRegistryResp, err := r.client.PostOrgsOrgIdRegistriesWithResponse(ctx, r.orgID, *request) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create registry, got error: %s", err)) + return + } + switch createRegistryResp.StatusCode() { + case http.StatusCreated: + registry = createRegistryResp.JSON201 + case http.StatusBadRequest: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create registry, Humanitec returned bad request: %s", createRegistryResp.Body)) + return + case http.StatusNotFound: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create registry, organization not found: %s", createRegistryResp.Body)) + return + default: + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to create registry unexpected status code: %d, body: %s", createRegistryResp.StatusCode(), createRegistryResp.Body)) + return + } + + diags = parseRegistryResponse(ctx, registry, data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ResourceRegistry) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *RegistryModel + + tflog.Info(ctx, fmt.Sprintf("start")) + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + tflog.Info(ctx, fmt.Sprintf("data: %v", data)) + + if resp.Diagnostics.HasError() { + return + } + + id := data.ID.ValueString() + + var registry *client.RegistryResponse + getRegistryResp, err := r.client.GetOrgsOrgIdRegistriesRegIdWithResponse(ctx, r.orgID, id) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to get registry, got error: %s", err)) + return + } + + tflog.Info(ctx, fmt.Sprintf("resp: %v", getRegistryResp.JSON200)) + switch getRegistryResp.StatusCode() { + case http.StatusOK: + registry = getRegistryResp.JSON200 + case http.StatusNotFound: + resp.Diagnostics.AddWarning("registry not found", fmt.Sprintf("The registry (%s) was deleted outside Terraform", id)) + resp.State.RemoveResource(ctx) + return + default: + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to get registry, unexpected status code: %d, body: %s", getRegistryResp.StatusCode(), getRegistryResp.Body)) + return + } + + diags := parseRegistryResponse(ctx, registry, data) + tflog.Info(ctx, fmt.Sprintf("parse: %v", data)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ResourceRegistry) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, state *RegistryModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + request, diags := parseRegistryModel(ctx, data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var registry *client.RegistryResponse + updateRegistryResp, err := r.client.PatchOrgsOrgIdRegistriesRegIdWithResponse(ctx, r.orgID, id, *request) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update registry, got error: %s", err)) + return + } + switch updateRegistryResp.StatusCode() { + case http.StatusOK: + registry = updateRegistryResp.JSON200 + case http.StatusBadRequest: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update registry, Humanitec returned bad request: %s", updateRegistryResp.Body)) + return + case http.StatusForbidden: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update humanitec build-in registry: %s", updateRegistryResp.Body)) + return + case http.StatusNotFound: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update registry, organization or registry not found: %s", updateRegistryResp.Body)) + return + case http.StatusConflict: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update registry, registry already registered: %s", updateRegistryResp.Body)) + return + default: + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to update registry, unexpected status code: %d, body: %s", updateRegistryResp.StatusCode(), updateRegistryResp.Body)) + return + } + + diags = parseRegistryResponse(ctx, registry, data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ResourceRegistry) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *RegistryModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id := data.ID.ValueString() + + deleteRegistryResp, err := r.client.DeleteOrgsOrgIdRegistriesRegIdWithResponse(ctx, r.orgID, id) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete registry, got error: %s", err)) + return + } + switch deleteRegistryResp.StatusCode() { + case http.StatusNoContent: + // Do nothing + case http.StatusForbidden: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete humanitec build-in registry: %s", deleteRegistryResp.Body)) + return + case http.StatusNotFound: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete registry, registry not found: %s", deleteRegistryResp.Body)) + return + default: + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to delete registry, unexpected status code: %d, body: %s", deleteRegistryResp.StatusCode(), deleteRegistryResp.Body)) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ResourceRegistry) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if req.ID == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + "Expected import identifier with registry id. Got an empty string", + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) +} + +func parseRegistryModel(ctx context.Context, data *RegistryModel) (*client.RegistryRequest, diag.Diagnostics) { + totalDiags := diag.Diagnostics{} + + var creds *client.AccountCredsRequest + if !data.Creds.IsNull() { + dataCreds := data.Creds.Attributes() + password := dataCreds["password"].String() + username := dataCreds["username"].String() + creds = &client.AccountCredsRequest{ + Password: password, + Username: username, + } + } + + var secrets *client.ClusterSecretsMapRequest + if data.Secrets != nil { + secretsMap := client.ClusterSecretsMapRequest{} + for key, value := range *data.Secrets { + secretsMap[key] = client.ClusterSecretRequest{ + Namespace: value.Namespace.ValueString(), + Secret: value.Secret.ValueString(), + } + } + secrets = &secretsMap + } + + return &client.RegistryRequest{ + Id: data.ID.ValueString(), + Registry: data.Registry.ValueString(), + Type: data.Type.ValueString(), + EnableCi: data.EnableCI.ValueBoolPointer(), + Creds: creds, + Secrets: secrets, + }, totalDiags +} + +func parseRegistryResponse(ctx context.Context, res *client.RegistryResponse, data *RegistryModel) diag.Diagnostics { + totalDiags := diag.Diagnostics{} + + data.ID = types.StringValue(res.Id) + data.Registry = types.StringValue(res.Registry) + data.Type = types.StringValue(res.Type) + data.EnableCI = types.BoolValue(res.EnableCi) + + if res.Secrets != nil { + secrets := make(map[string]SecretsModel) + for key, value := range *res.Secrets { + secrets[key] = SecretsModel{ + Namespace: types.StringValue(value.Namespace), + Secret: types.StringValue(value.Secret), + } + } + data.Secrets = &secrets + } + + return totalDiags +} diff --git a/internal/provider/resource_registry_test.go b/internal/provider/resource_registry_test.go new file mode 100644 index 0000000..a20c098 --- /dev/null +++ b/internal/provider/resource_registry_test.go @@ -0,0 +1,67 @@ +package provider + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccResourceRegistry(t *testing.T) { + id := fmt.Sprintf("test-%d", time.Now().UnixNano()) + registry := fmt.Sprintf("test-%d.com.pl", time.Now().UnixNano()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccResourceRegistry(id, registry, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("humanitec_registry.registry_test", "id", id), + resource.TestCheckResourceAttr("humanitec_registry.registry_test", "registry", registry), + resource.TestCheckResourceAttr("humanitec_registry.registry_test", "enable_ci", "false"), + ), + }, + // ImportState testing + { + ResourceName: "humanitec_registry.registry_test", + ImportStateId: id, + ImportState: true, + ImportStateVerify: true, + }, + // Update testing + { + Config: testAccResourceRegistry(id, registry, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("humanitec_registry.registry_test", "id", id), + resource.TestCheckResourceAttr("humanitec_registry.registry_test", "registry", registry), + resource.TestCheckResourceAttr("humanitec_registry.registry_test", "enable_ci", "true"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccResourceRegistry(id, registry string, enable_ci bool) string { + return fmt.Sprintf(` +resource "humanitec_registry" "registry_test" { + id = "%s" + registry = "%s" + type = "secret_ref" + enable_ci = %t + secrets = { + "cluster-a" = { + namespace = "example-namespace" + secret = "path/to/secret" + }, + "cluster-b" = { + namespace = "example-namespace" + secret = "path/to/secret" + } + } +}`, id, registry, enable_ci) +}