diff --git a/.golangci.yml b/.golangci.yml index cd00228..dc6c93c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,13 @@ -# Copyright 2021 Nordcloud Oy or its affiliates. All Rights Reserved. +# Copyright 2021-2024 Nordcloud Oy or its affiliates. All Rights Reserved. run: concurrency: 4 timeout: 6m - issues-exit-code: 0 - # Put here project specific + issues-exit-code: 0 + # Put here project specific skip-dirs: - ui - - api + - api - gql - build - artifacts @@ -73,7 +73,6 @@ linters: enable: - bodyclose - deadcode - - depguard - dogsled - dupl - errcheck diff --git a/docs/index.md b/docs/index.md index d7be12c..0f457d2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,7 +26,7 @@ Please check https://klarity.nordcloud.com to learn more about the ImageFactory ```terraform /** - * Copyright 2021-2023 Nordcloud Oy or its affiliates. All Rights Reserved. + * Copyright 2021-2024 Nordcloud Oy or its affiliates. All Rights Reserved. */ terraform { @@ -34,7 +34,7 @@ terraform { required_providers { imagefactory = { source = "nordcloud/imagefactory" - version = "1.8.4" + version = "1.8.5" } } } diff --git a/docs/resources/custom_component.md b/docs/resources/custom_component.md index 35eff37..ac50b7a 100644 --- a/docs/resources/custom_component.md +++ b/docs/resources/custom_component.md @@ -38,32 +38,18 @@ output "shell_component" { # An example of a Powershell component resource "imagefactory_custom_component" "powershell_component" { - name = "Install Apache" - description = "Install Apache HTTP Server on Microsoft Windows" + name = "Install nginx" + description = "Install nginx Server on Microsoft Windows" stage = "BUILD" cloud_providers = ["AWS", "AZURE"] os_types = ["WINDOWS"] content { script = <<-EOT - --- - - name: Installing Apache HTTP Server - hosts: all - - tasks: - - name: Create directory structure - ansible.windows.win_file: - path: C:\ansible_examples - state: directory - - - name: Download the Apache installer - win_get_url: - url: https://archive.apache.org/dist/httpd/binaries/win32/httpd-2.2.25-win32-x86-no_ssl.msi - dest: C:\ansible_examples\httpd-2.2.25-win32-x86-no_ssl.msi - - - name: Install MSI - win_package: - path: C:\ansible_examples\httpd-2.2.25-win32-x86-no_ssl.msi - state: present + $ErrorActionPreference = 'Stop'; + $ProgressPreference = 'SilentlyContinue'; + Invoke-WebRequest -Method Get -Uri https://nginx.org/download/nginx-1.25.4.zip -OutFile c:\nginx-1.25.4.zip ; + Expand-Archive -Path c:\nginx-1.25.4.zip -DestinationPath c:\ ; + Remove-Item c:\nginx-1.25.4.zip -Force EOT provisioner = "POWERSHELL" } @@ -123,6 +109,7 @@ output "ansible_component" { ### Optional - `description` (String) +- `rebuild_templates` (Boolean) Trigger rebuild of templates using this component. Only applicable when content is updated. Default is false. ### Read-Only diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 076dcd5..a5bbd96 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -7,7 +7,7 @@ terraform { required_providers { imagefactory = { source = "nordcloud/imagefactory" - version = "1.8.4" + version = "1.8.5" } } } diff --git a/examples/resources/imagefactory_custom_component/resource.tf b/examples/resources/imagefactory_custom_component/resource.tf index f0aeee4..ac19c98 100644 --- a/examples/resources/imagefactory_custom_component/resource.tf +++ b/examples/resources/imagefactory_custom_component/resource.tf @@ -1,4 +1,4 @@ -// Copyright 2021-2023 Nordcloud Oy or its affiliates. All Rights Reserved. +// Copyright 2021-2024 Nordcloud Oy or its affiliates. All Rights Reserved. # An example of a SHELL component @@ -23,32 +23,18 @@ output "shell_component" { # An example of a Powershell component resource "imagefactory_custom_component" "powershell_component" { - name = "Install Apache" - description = "Install Apache HTTP Server on Microsoft Windows" + name = "Install nginx" + description = "Install nginx Server on Microsoft Windows" stage = "BUILD" cloud_providers = ["AWS", "AZURE"] os_types = ["WINDOWS"] content { script = <<-EOT - --- - - name: Installing Apache HTTP Server - hosts: all - - tasks: - - name: Create directory structure - ansible.windows.win_file: - path: C:\ansible_examples - state: directory - - - name: Download the Apache installer - win_get_url: - url: https://archive.apache.org/dist/httpd/binaries/win32/httpd-2.2.25-win32-x86-no_ssl.msi - dest: C:\ansible_examples\httpd-2.2.25-win32-x86-no_ssl.msi - - - name: Install MSI - win_package: - path: C:\ansible_examples\httpd-2.2.25-win32-x86-no_ssl.msi - state: present + $ErrorActionPreference = 'Stop'; + $ProgressPreference = 'SilentlyContinue'; + Invoke-WebRequest -Method Get -Uri https://nginx.org/download/nginx-1.25.4.zip -OutFile c:\nginx-1.25.4.zip ; + Expand-Archive -Path c:\nginx-1.25.4.zip -DestinationPath c:\ ; + Remove-Item c:\nginx-1.25.4.zip -Force EOT provisioner = "POWERSHELL" } diff --git a/imagefactory/component/resource.go b/imagefactory/component/resource.go index 06bbfeb..d8a1dd0 100644 --- a/imagefactory/component/resource.go +++ b/imagefactory/component/resource.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 Nordcloud Oy or its affiliates. All Rights Reserved. +// Copyright 2021-2024 Nordcloud Oy or its affiliates. All Rights Reserved. package component @@ -37,7 +37,7 @@ func resourceComponentCreate(ctx context.Context, d *schema.ResourceData, m inte Providers: expandProviders(d.Get("cloud_providers").([]interface{})), Content: expandContent(d.Get("content").([]interface{})), } - if len(d.Get("description").(string)) > 0 { + if d.Get("description").(string) != "" { description := graphql.String(d.Get("description").(string)) input.Description = &description } @@ -79,7 +79,7 @@ func resourceComponentUpdate(ctx context.Context, d *schema.ResourceData, m inte OsTypes: expandOSTypes(d.Get("os_types").([]interface{})), Providers: expandProviders(d.Get("cloud_providers").([]interface{})), } - if len(d.Get("description").(string)) > 0 { + if d.Get("description").(string) != "" { description := graphql.String(d.Get("description").(string)) input.Description = &description } @@ -104,6 +104,12 @@ func resourceComponentUpdate(ctx context.Context, d *schema.ResourceData, m inte if err != nil { return diag.FromErr(err) } + + if d.Get("rebuild_templates").(bool) { + if err := c.APIClient.RebuildTemplatesUsingComponent(componentID); err != nil { + return diag.FromErr(err) + } + } } return setProps(d, component) diff --git a/imagefactory/component/schema.go b/imagefactory/component/schema.go index 43bd9f6..065492b 100644 --- a/imagefactory/component/schema.go +++ b/imagefactory/component/schema.go @@ -1,4 +1,4 @@ -// Copyright 2021 Nordcloud Oy or its affiliates. All Rights Reserved. +// Copyright 2021-2024 Nordcloud Oy or its affiliates. All Rights Reserved. package component @@ -88,4 +88,10 @@ var componentSchema = map[string]*schema.Schema{ Required: true, Elem: contentComponentResource, }, + "rebuild_templates": { + Type: schema.TypeBool, + Optional: true, + Description: "Trigger rebuild of templates using this component. " + + "Only applicable when content is updated. Default is false.", + }, } diff --git a/pkg/graphql/graphql.go b/pkg/graphql/graphql.go index 5ae540b..45ff0ca 100644 --- a/pkg/graphql/graphql.go +++ b/pkg/graphql/graphql.go @@ -2316,6 +2316,16 @@ type GetTemplatesResponse struct { Name string `json:"name"` } `json:"results"` } `json:"images"` + Config struct { + BuildComponents *[]struct { + ID string `json:"id"` + Version string `json:"version"` + } `json:"buildComponents"` + TestComponents *[]struct { + ID string `json:"id"` + Version string `json:"version"` + } `json:"testComponents"` + } `json:"config"` } `json:"results"` } `json:"templates"` } @@ -2347,6 +2357,16 @@ func NewGetTemplatesRequest(url string, vars *GetTemplatesVariables) (*GetTempla name } } + config { + buildComponents { + id + version + } + testComponents { + id + version + } + } } } }`, @@ -2608,6 +2628,72 @@ func (client *Client) DeleteTemplate(vars *DeleteTemplateVariables) (*DeleteTemp return DeleteTemplate(client.Url, client.Client, vars) } +// +// mutation RebuildTemplate($input: CustomerTemplateIdInput!) +// + +type RebuildTemplateVariables struct { + Input CustomerTemplateIdInput `json:"input"` +} + +type RebuildTemplateResponse struct { + RebuildTemplate struct { + ID string `json:"id"` + } `json:"rebuildTemplate"` +} + +type RebuildTemplateRequest struct { + *http.Request +} + +func NewRebuildTemplateRequest(url string, vars *RebuildTemplateVariables) (*RebuildTemplateRequest, error) { + variables, err := json.Marshal(vars) + if err != nil { + return nil, err + } + b, err := json.Marshal(&GraphQLOperation{ + Variables: variables, + Query: `mutation RebuildTemplate($input: CustomerTemplateIdInput!) { + rebuildTemplate(input: $input) { + id + } +}`, + }) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + return &RebuildTemplateRequest{req}, nil +} + +func (req *RebuildTemplateRequest) Execute(client *http.Client) (*RebuildTemplateResponse, error) { + resp, err := execute(client, req.Request) + if err != nil { + return nil, err + } + var result RebuildTemplateResponse + if err := json.Unmarshal(resp.Data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func RebuildTemplate(url string, client *http.Client, vars *RebuildTemplateVariables) (*RebuildTemplateResponse, error) { + req, err := NewRebuildTemplateRequest(url, vars) + if err != nil { + return nil, err + } + return req.Execute(client) +} + +func (client *Client) RebuildTemplate(vars *RebuildTemplateVariables) (*RebuildTemplateResponse, error) { + return RebuildTemplate(client.Url, client.Client, vars) +} + // // query GetVariables // diff --git a/pkg/graphql/template.graphql b/pkg/graphql/template.graphql index db0c14a..6113d2b 100644 --- a/pkg/graphql/template.graphql +++ b/pkg/graphql/template.graphql @@ -1,4 +1,4 @@ -# Copyright 2021-2023 Nordcloud Oy or its affiliates. All Rights Reserved. +# Copyright 2021-2024 Nordcloud Oy or its affiliates. All Rights Reserved. query GetTemplate($input: CustomerTemplateIdInput!) { template(input: $input) { @@ -30,6 +30,16 @@ query GetTemplates($input: CustomerTemplatesInput!) { name } } + config { + buildComponents { + id + version + } + testComponents { + id + version + } + } } } } @@ -63,3 +73,9 @@ mutation UpdateTemplate($input: TemplateChanges!) { mutation DeleteTemplate($input: CustomerTemplateIdInput!) { deleteTemplate(input: $input) } + +mutation RebuildTemplate($input: CustomerTemplateIdInput!) { + rebuildTemplate(input: $input) { + id + } +} diff --git a/pkg/sdk/api.go b/pkg/sdk/api.go index b119c46..627b749 100644 --- a/pkg/sdk/api.go +++ b/pkg/sdk/api.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 Nordcloud Oy or its affiliates. All Rights Reserved. +// Copyright 2021-2024 Nordcloud Oy or its affiliates. All Rights Reserved. package sdk @@ -57,4 +57,7 @@ type API interface { CreateVariable(input NewVariable) (Variable, error) UpdateVariable(input NewVariable) (Variable, error) DeleteVariable(name string) error + + // Actions + RebuildTemplatesUsingComponent(componentID string) error } diff --git a/pkg/sdk/client.go b/pkg/sdk/client.go index 4333ec2..51adc87 100644 --- a/pkg/sdk/client.go +++ b/pkg/sdk/client.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 Nordcloud Oy or its affiliates. All Rights Reserved. +// Copyright 2021-2024 Nordcloud Oy or its affiliates. All Rights Reserved. package sdk @@ -499,9 +499,10 @@ func (c APIClient) GetAPIKeyByName(name string) (APIKey, error) { } if r.ApiKeys.Results != nil { - for _, k := range *r.ApiKeys.Results { - if string(k.Name) == name { - return APIKey(k), nil + for i := range *r.ApiKeys.Results { + apiKey := (*r.ApiKeys.Results)[i] + if string(apiKey.Name) == name { + return APIKey(apiKey), nil } } } @@ -686,3 +687,70 @@ func (c APIClient) DeleteVariable(name string) error { return nil } + +func (c APIClient) RebuildTemplatesUsingComponent(componentID string) error { + req, err := graphql.NewGetTemplatesRequest(c.apiURL, &graphql.GetTemplatesVariables{ + Input: graphql.CustomerTemplatesInput{ + Search: &graphql.Search{ + SearchValue: graphql.String(componentID), + Algorithm: graphql.SearchAlgorithmSUBSTRINGMATCH, + }, + }, + }) + if err != nil { + return fmt.Errorf("getting templates request %w", err) + } + + r := &graphql.Query{} + if err := c.graphqlAPI.Execute(req.Request, r); err != nil { + return fmt.Errorf("getting templates %w", err) + } + if r.Templates.Results == nil { + return nil + } + + for i := range *r.Templates.Results { + template := (*r.Templates.Results)[i] + + // ignore templates that have the component pinned to a specific version + if isNotLatestComponent(componentID, template.Config) { + continue + } + + req, err := graphql.NewRebuildTemplateRequest(c.apiURL, &graphql.RebuildTemplateVariables{ + Input: graphql.CustomerTemplateIdInput{ + TemplateId: template.ID, + }, + }) + if err != nil { + return fmt.Errorf("getting rebuild template request %w", err) + } + + r := &graphql.Mutation{} + if err := c.graphqlAPI.Execute(req.Request, r); err != nil { + return fmt.Errorf("rebuilding template ID: %s err: %w", template.ID, err) + } + } + + return nil +} + +func isNotLatestComponent(componentID string, config *graphql.TemplateConfig) bool { + const latest = "latest" + + if config == nil { + return false + } + + for _, components := range []*[]graphql.TemplateComponent{config.BuildComponents, config.TestComponents} { + if components != nil { + for _, bc := range *components { + if bc.ID == graphql.String(componentID) && bc.Version != nil && *bc.Version != latest { + return true + } + } + } + } + + return false +}