From e02807537b1340d352ecd5da237f11e5978b0662 Mon Sep 17 00:00:00 2001 From: michaelhtm <98621731+michaelhtm@users.noreply.github.com> Date: Mon, 18 May 2026 14:01:44 -0700 Subject: [PATCH] feat: run full generation pipeline from controller repos via ack-generate Replace shell scripts (build-controller.sh, build-controller-release.sh) with a Go-native pipeline that runs from within the controller repo. The `ack-generate controller` command now handles APIs, deepcopy, CRDs, RBAC, formatting, and boilerplate in a single invocation. Templates and boilerplate files are embedded via go:embed, enabling `go install` without a local code-generator checkout. --- Makefile | 22 +- cmd/ack-generate/command/apis.go | 147 ------- cmd/ack-generate/command/common.go | 196 +++++++-- cmd/ack-generate/command/controller.go | 101 +++-- cmd/ack-generate/command/crossplane.go | 5 + cmd/ack-generate/command/olm.go | 12 +- cmd/ack-generate/command/release.go | 75 ++-- cmd/ack-generate/command/root.go | 17 + embed.go | 22 + pkg/generate/ack/apis.go | 73 +--- pkg/generate/ack/controller.go | 84 +++- pkg/generate/ack/hook.go | 24 ++ pkg/generate/ack/hook_test.go | 4 +- pkg/generate/ack/pipeline.go | 520 ++++++++++++++++++++++++ pkg/generate/ack/release.go | 4 +- pkg/generate/olm/olm.go | 4 +- pkg/generate/templateset/templateset.go | 82 +++- pkg/metadata/generation_metadata.go | 15 + pkg/sdk/repo.go | 5 +- pkg/sdk/runtime.go | 162 ++++++++ pkg/version/version.go | 63 +++ scripts/build-controller-release.sh | 299 -------------- scripts/build-controller.sh | 254 ------------ templates/Makefile.tpl | 66 +++ 24 files changed, 1298 insertions(+), 958 deletions(-) delete mode 100644 cmd/ack-generate/command/apis.go create mode 100644 embed.go create mode 100644 pkg/generate/ack/pipeline.go create mode 100644 pkg/sdk/runtime.go delete mode 100755 scripts/build-controller-release.sh delete mode 100755 scripts/build-controller.sh create mode 100644 templates/Makefile.tpl diff --git a/Makefile b/Makefile index fa4e3a17..77bea810 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,8 @@ GO_LDFLAGS=-ldflags "-X $(IMPORT_PATH)/pkg/version.Version=$(VERSION) \ # aws-sdk-go/private/model/api package is gated behind a build tag "codegen"... GO_CMD_FLAGS=-tags codegen -.PHONY: all build-ack-generate test \ - build-controller build-controller-image \ +.PHONY: all build-ack-generate build-controller test \ + build-controller-image \ local-build-controller-image lint-shell \ check-crd-compatibility @@ -30,12 +30,18 @@ build-ack-generate: ## Build ack-generate binary @go build ${GO_CMD_FLAGS} ${GO_LDFLAGS} -o bin/ack-generate cmd/ack-generate/main.go @echo "ok." -build-controller: build-ack-generate ## Generate controller code for SERVICE - @./scripts/install-controller-gen.sh - @echo "==== building $(AWS_SERVICE)-controller ====" - @./scripts/build-controller.sh $(AWS_SERVICE) - @echo "==== building $(AWS_SERVICE)-controller release artifacts ====" - @./scripts/build-controller-release.sh $(AWS_SERVICE) +CONTROLLER_PATH ?= ../$(AWS_SERVICE)-controller +RELEASE_VERSION ?= $(shell cd $(CONTROLLER_PATH) && git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0-non-release-version") + +ACK_GENERATE_OLM ?= false +OLM_CONFIG_PATH ?= $(CONTROLLER_PATH)/olm/olmconfig.yaml + +build-controller: build-ack-generate ## Generate a service controller (SERVICE=s3) + @$(CURDIR)/bin/ack-generate controller $(AWS_SERVICE) -o $(CONTROLLER_PATH) + @$(CURDIR)/bin/ack-generate release $(AWS_SERVICE) $(RELEASE_VERSION) -o $(CONTROLLER_PATH) +ifeq ($(ACK_GENERATE_OLM),true) + @$(CURDIR)/bin/ack-generate olm $(AWS_SERVICE) $(RELEASE_VERSION) -o $(CONTROLLER_PATH) --olm-config $(OLM_CONFIG_PATH) +endif build-controller-image: export LOCAL_MODULES = false build-controller-image: ## Build container image for SERVICE diff --git a/cmd/ack-generate/command/apis.go b/cmd/ack-generate/command/apis.go deleted file mode 100644 index dace0a91..00000000 --- a/cmd/ack-generate/command/apis.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package command - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "strings" - "time" - - "github.com/spf13/cobra" - - ackgenerate "github.com/aws-controllers-k8s/code-generator/pkg/generate/ack" - ackmetadata "github.com/aws-controllers-k8s/code-generator/pkg/metadata" - "github.com/aws-controllers-k8s/code-generator/pkg/sdk" - "github.com/aws-controllers-k8s/code-generator/pkg/util" -) - -type contentType int - -const ( - ctUnknown contentType = iota - ctJSON - ctYAML -) - -var ( - optGenVersion string - optAPIsInputPath string - apisVersionPath string -) - -// apiCmd is the command that generates service API types -var apisCmd = &cobra.Command{ - Use: "apis ", - Short: "Generate Kubernetes API type definitions for an AWS service API", - RunE: generateAPIs, - PostRunE: saveGeneratedMetadata, -} - -func init() { - apisCmd.PersistentFlags().StringVar( - &optGenVersion, "version", "v1alpha1", "the resource API Version to use when generating API infrastructure and type definitions", - ) - rootCmd.AddCommand(apisCmd) -} - -// saveGeneratedMetadata saves the parameters used to generate APIs and checksum -// of the generated code. -func saveGeneratedMetadata(cmd *cobra.Command, args []string) error { - err := ackmetadata.CreateGenerationMetadata( - optGenVersion, - filepath.Join(optOutputPath, "apis"), - ackmetadata.UpdateReasonAPIGeneration, - optAWSSDKGoVersion, - optGeneratorConfigPath, - ) - if err != nil { - return fmt.Errorf("cannot create generation metadata file: %v", err) - } - - copyDest := filepath.Join( - optOutputPath, "apis", optGenVersion, "generator.yaml", - ) - err = util.CopyFile(optGeneratorConfigPath, copyDest) - if err != nil { - return fmt.Errorf("cannot copy generator configuration file: %v", err) - } - - return nil -} - -// generateAPIs generates the Go files for each resource in the AWS service -// API. -func generateAPIs(cmd *cobra.Command, args []string) error { - cmdStart := time.Now() - if len(args) != 1 { - return fmt.Errorf("please specify the service alias for the AWS service API to generate") - } - svcAlias := strings.ToLower(args[0]) - if optOutputPath == "" { - optOutputPath = filepath.Join(optServicesDir, svcAlias) - } - - // Load generator config to resolve model name before fetching - cfg, err := setupGenerator(svcAlias) - if err != nil { - return err - } - - modelStart := time.Now() - metadata, err := ackmetadata.NewServiceMetadata(optMetadataConfigPath) - if err != nil { - return err - } - m, err := loadModelWithLatestAPIVersion(svcAlias, metadata, cfg) - if err != nil { - return err - } - util.Tracef("loadModel: %s\n", time.Since(modelStart)) - - apisStart := time.Now() - ts, err := ackgenerate.APIs(m, optTemplateDirs) - if err != nil { - return err - } - util.Tracef("APIs() template setup: %s\n", time.Since(apisStart)) - - execStart := time.Now() - if err = ts.Execute(); err != nil { - return err - } - util.Tracef("template execution: %s\n", time.Since(execStart)) - - writeStart := time.Now() - apisVersionPath = filepath.Join(optOutputPath, "apis", optGenVersion) - for path, contents := range ts.Executed() { - if optDryRun { - fmt.Printf("============================= %s ======================================\n", path) - fmt.Println(strings.TrimSpace(contents.String())) - continue - } - outPath := filepath.Join(apisVersionPath, path) - outDir := filepath.Dir(outPath) - if _, err := sdk.EnsureDir(outDir); err != nil { - return err - } - if err = ioutil.WriteFile(outPath, contents.Bytes(), 0666); err != nil { - return err - } - } - util.Tracef("file writing (%d files): %s\n", len(ts.Executed()), time.Since(writeStart)) - util.Tracef("generateAPIs total: %s\n", time.Since(cmdStart)) - return nil -} diff --git a/cmd/ack-generate/command/common.go b/cmd/ack-generate/command/common.go index b98f1f13..8fa1ecc2 100644 --- a/cmd/ack-generate/command/common.go +++ b/cmd/ack-generate/command/common.go @@ -16,21 +16,136 @@ package command import ( "context" "fmt" + "os" + "path/filepath" "sort" "strings" - "time" k8sversion "k8s.io/apimachinery/pkg/version" ackgenconfig "github.com/aws-controllers-k8s/code-generator/pkg/config" ackgenerate "github.com/aws-controllers-k8s/code-generator/pkg/generate/ack" + "github.com/aws-controllers-k8s/code-generator/pkg/generate/templateset" ackmetadata "github.com/aws-controllers-k8s/code-generator/pkg/metadata" ackmodel "github.com/aws-controllers-k8s/code-generator/pkg/model" - "github.com/aws-controllers-k8s/code-generator/pkg/sdk" acksdk "github.com/aws-controllers-k8s/code-generator/pkg/sdk" - "github.com/aws-controllers-k8s/code-generator/pkg/util" ) +// controllerContext holds the resolved state shared between the controller +// and release commands. +type controllerContext struct { + SvcAlias string + ControllerPath string + APIVersion string + RuntimeVersion string + RBACRoleName string + Model *ackmodel.Model + Metadata *ackmetadata.ServiceMetadata +} + +// resolveControllerContext performs the common setup for commands that run +// from a controller repo: resolves config paths, loads the model, and +// determines the API version and runtime version. +func resolveControllerContext(svcAlias string) (*controllerContext, error) { + controllerPath := optOutputPath + if controllerPath == "" { + var err error + controllerPath, err = os.Getwd() + if err != nil { + return nil, fmt.Errorf("determining current directory: %w", err) + } + } + controllerPath, _ = filepath.Abs(controllerPath) + optOutputPath = controllerPath + + if _, err := os.Stat(filepath.Join(controllerPath, "go.mod")); err != nil { + return nil, fmt.Errorf("path does not appear to be a controller repo (no go.mod found): %s", controllerPath) + } + + resolved := ackgenerate.ResolveConfigPaths(controllerPath) + if optGeneratorConfigPath == "" { + optGeneratorConfigPath = resolved.GeneratorConfigPath + } + if optMetadataConfigPath == "" { + optMetadataConfigPath = resolved.MetadataConfigPath + } + if optDocumentationConfigPath == "" { + optDocumentationConfigPath = resolved.DocumentationConfigPath + } + if optServiceAccountName == "" { + optServiceAccountName = fmt.Sprintf("ack-%s-controller", svcAlias) + } + + // Detect template overrides in the controller repo + svcTemplatesDir := filepath.Join(controllerPath, "templates") + if fi, err := os.Stat(svcTemplatesDir); err == nil && fi.IsDir() { + optTemplateDirs = append([]string{svcTemplatesDir}, optTemplateDirs...) + } + + // Load service metadata early so we can determine the API version + // and read the last-generation SDK version from ack-generate-metadata.yaml. + metadata, err := ackmetadata.NewServiceMetadata(optMetadataConfigPath) + if err != nil { + return nil, err + } + + apiVersion := "v1alpha1" + if len(metadata.APIVersions) > 0 { + av, err := getLatestAPIVersion(metadata.APIVersions) + if err == nil { + apiVersion = av + } + } + + cfg, err := setupGenerator(svcAlias) + if err != nil { + return nil, err + } + + m, err := loadModelWithLatestAPIVersion(svcAlias, metadata, cfg) + if err != nil { + return nil, err + } + + runtimeVersion, err := acksdk.GetRuntimeVersion(controllerPath) + if err != nil { + return nil, fmt.Errorf("resolving runtime version: %w", err) + } + + return &controllerContext{ + SvcAlias: svcAlias, + ControllerPath: controllerPath, + APIVersion: apiVersion, + RuntimeVersion: runtimeVersion, + RBACRoleName: fmt.Sprintf("ack-%s-controller", svcAlias), + Model: m, + Metadata: metadata, + }, nil +} + +// writeTemplateSet executes a template set and writes the output files to +// the given base directory. If optDryRun is set, prints to stdout instead. +func writeTemplateSet(ts *templateset.TemplateSet, baseDir string) error { + if err := ts.Execute(); err != nil { + return err + } + for path, contents := range ts.Executed() { + if optDryRun { + fmt.Printf("============================= %s ======================================\n", path) + fmt.Println(strings.TrimSpace(contents.String())) + continue + } + outPath := filepath.Join(baseDir, path) + if _, err := acksdk.EnsureDir(filepath.Dir(outPath)); err != nil { + return err + } + if err := os.WriteFile(outPath, contents.Bytes(), 0666); err != nil { + return err + } + } + return nil +} + // resolveModelName returns the SDK model name for a service, checking the // generator config for an override. func resolveModelName(svcAlias string, cfg ackgenconfig.Config) string { @@ -52,20 +167,15 @@ func loadModelWithLatestAPIVersion(svcAlias string, metadata *ackmetadata.Servic } // loadModel finds the AWS SDK for a given service alias and creates a new model -// with the given API version. The cfg parameter should be pre-loaded by the -// caller so that the model name can be resolved before fetching. +// with the given API version. func loadModel(svcAlias string, apiVersion string, apiGroup string, cfg ackgenconfig.Config) (*ackmodel.Model, error) { - totalStart := time.Now() - modelName := resolveModelName(svcAlias, cfg) - apiStart := time.Now() sdkHelper := acksdk.NewHelper(sdkDir, cfg) sdkAPI, err := sdkHelper.API(modelName) if err != nil { return nil, err } - util.Tracef("SDK API loading (model=%s): %s\n", modelName, time.Since(apiStart)) if apiGroup != "" { sdkAPI.APIGroupSuffix = apiGroup @@ -76,24 +186,14 @@ func loadModel(svcAlias string, apiVersion string, apiGroup string, cfg ackgenco return nil, err } - modelStart := time.Now() - m, err := ackmodel.New( - sdkAPI, svcAlias, apiVersion, cfg, docCfg, - ) - if err != nil { - return nil, err - } - util.Tracef("model.New(): %s\n", time.Since(modelStart)) - util.Tracef("loadModel total: %s\n", time.Since(totalStart)) - return m, nil + return ackmodel.New(sdkAPI, svcAlias, apiVersion, cfg, docCfg) } -// getLatestAPIVersion looks in the controller metadata file to determine what -// the latest Kubernetes API version for CRDs exposed by the generated service +// getLatestAPIVersion looks in the controller metadata file to determine the +// latest Kubernetes API version for CRDs exposed by the generated service // controller. func getLatestAPIVersion(apiVersions []ackmetadata.ServiceVersion) (string, error) { versions := []string{} - for _, version := range apiVersions { versions = append(versions, version.APIVersion) } @@ -103,42 +203,56 @@ func getLatestAPIVersion(apiVersions []ackmetadata.ServiceVersion) (string, erro return versions[len(versions)-1], nil } -// getServiceAccountName gets the service account name from the optional flag passed into ack-generate -func getServiceAccountName() (string, error) { - if optServiceAccountName != "" { - return optServiceAccountName, nil - } - - return "", fmt.Errorf("service account name not set") -} - -// setupGenerator loads the generator configuration, resolves the SDK version and fetches the -// model file +// setupGenerator loads the generator configuration, resolves the SDK version +// and fetches the model file. func setupGenerator(svcAlias string) (ackgenconfig.Config, error) { - // Load generator config to resolve model name before fetching cfg, err := ackgenconfig.New(optGeneratorConfigPath, ackgenerate.DefaultConfig) if err != nil { return cfg, err } - // Resolve SDK version and fetch the model file - fetchStart := time.Now() - resolvedVersion, err := sdk.GetSDKVersion(optAWSSDKGoVersion, "", optOutputPath) + // Read the last-used SDK version from ack-generate-metadata.yaml + lastGenVersion := findLastSDKVersion(optOutputPath) + + resolvedVersion, err := acksdk.GetSDKVersion(optAWSSDKGoVersion, lastGenVersion, optOutputPath) if err != nil { return cfg, err } - resolvedVersion = sdk.EnsureSemverPrefix(resolvedVersion) + resolvedVersion = acksdk.EnsureSemverPrefix(resolvedVersion) + fmt.Printf("Using AWS SDK version %s\n", resolvedVersion) modelName := resolveModelName(svcAlias, cfg) - ctx, cancel := sdk.ContextWithSigterm(context.Background()) + ctx, cancel := acksdk.ContextWithSigterm(context.Background()) defer cancel() - basePath, err := sdk.EnsureModel(ctx, optCacheDir, resolvedVersion, modelName) + basePath, err := acksdk.EnsureModel(ctx, optCacheDir, resolvedVersion, modelName) if err != nil { return cfg, err } sdkDir = basePath sdkVersion = resolvedVersion - util.Tracef("EnsureModel: %s\n", time.Since(fetchStart)) return cfg, nil } + +// findLastSDKVersion scans the apis/ directory for an ack-generate-metadata.yaml +// file and returns the AWS SDK version recorded in it. Returns "" if not found. +func findLastSDKVersion(outputPath string) string { + if outputPath == "" { + return "" + } + apisPath := filepath.Join(outputPath, "apis") + entries, err := os.ReadDir(apisPath) + if err != nil { + return "" + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + meta, err := ackmetadata.ReadGenerationMetadata(apisPath, entry.Name()) + if err == nil && meta.AWSSDKGoVersion != "" { + return meta.AWSSDKGoVersion + } + } + return "" +} diff --git a/cmd/ack-generate/command/controller.go b/cmd/ack-generate/command/controller.go index 487b8fff..8dc403da 100644 --- a/cmd/ack-generate/command/controller.go +++ b/cmd/ack-generate/command/controller.go @@ -14,11 +14,10 @@ package command import ( + "context" "fmt" - "io/ioutil" "path/filepath" "strings" - "time" "github.com/spf13/cobra" @@ -28,85 +27,81 @@ import ( "github.com/aws-controllers-k8s/code-generator/pkg/util" ) -var ( - cmdControllerPath string - pkgResourcePath string - latestAPIVersion string -) - var controllerCmd = &cobra.Command{ Use: "controller ", - Short: "Generates Go files containing service controller implementation for a given service", - RunE: generateController, + Short: "Generates a fully-built service controller from the current directory", + Long: `Runs the full generation pipeline for a controller repo. By default uses +the current working directory; use -o to specify the controller path. +This includes APIs, deepcopy, CRDs, controller code, RBAC, formatting, +and boilerplate file copying. + +Usage: + ack-generate controller s3 -o /path/to/s3-controller + # or from within the controller repo: + ack-generate controller s3`, + RunE: generateController, } func init() { rootCmd.AddCommand(controllerCmd) } -// generateController generates the Go files for a service controller func generateController(cmd *cobra.Command, args []string) error { - cmdStart := time.Now() if len(args) != 1 { return fmt.Errorf("please specify the service alias for the AWS service API to generate") } svcAlias := strings.ToLower(args[0]) - if optOutputPath == "" { - optOutputPath = filepath.Join(optServicesDir, svcAlias) - } - // Load generator config to resolve model name before fetching - cfg, err := setupGenerator(svcAlias) + ctx, cancel := sdk.ContextWithSigterm(context.Background()) + defer cancel() + + controllerCtx, err := resolveControllerContext(svcAlias) if err != nil { return err } - modelStart := time.Now() - metadata, err := ackmetadata.NewServiceMetadata(optMetadataConfigPath) - if err != nil { - return err + pipelineOpts := ackgenerate.BuildControllerOptions{ + SvcAlias: controllerCtx.SvcAlias, + ControllerSourcePath: controllerCtx.ControllerPath, + APIVersion: controllerCtx.APIVersion, + RBACRoleName: controllerCtx.RBACRoleName, + RuntimeVersion: controllerCtx.RuntimeVersion, + ControllerGenVersion: optControllerGenVersion, + CacheDir: optCacheDir, + BoilerplateFS: embeddedBoilerplateFS, + TemplatesFS: embeddedTemplatesFS, } - m, err := loadModelWithLatestAPIVersion(svcAlias, metadata, cfg) + + // Step 1: Generate and write all templates (APIs + controller code) + fmt.Printf("Building service controller for %s\n", svcAlias) + ts, err := ackgenerate.Controller( + controllerCtx.Model, optTemplateDirs, optServiceAccountName, + controllerCtx.APIVersion, embeddedTemplatesFS, + ) if err != nil { return err } - util.Tracef("loadModel: %s\n", time.Since(modelStart)) - - serviceAccountName, err := getServiceAccountName() - if err != nil { + if err := writeTemplateSet(ts, optOutputPath); err != nil { return err } - ctrlStart := time.Now() - ts, err := ackgenerate.Controller(m, optTemplateDirs, serviceAccountName) - if err != nil { - return err + // Save generation metadata and copy generator.yaml + apisPath := filepath.Join(optOutputPath, "apis") + if err := ackmetadata.CreateGenerationMetadata( + controllerCtx.APIVersion, apisPath, ackmetadata.UpdateReasonAPIGeneration, + sdkVersion, optGeneratorConfigPath, + ); err != nil { + return fmt.Errorf("creating generation metadata: %w", err) + } + if err := util.CopyFile(optGeneratorConfigPath, filepath.Join(apisPath, controllerCtx.APIVersion, "generator.yaml")); err != nil { + return fmt.Errorf("copying generator.yaml: %w", err) } - util.Tracef("Controller() template setup: %s\n", time.Since(ctrlStart)) - execStart := time.Now() - if err = ts.Execute(); err != nil { + // Step 2: Pre-codegen pipeline (runtime CRDs, deepcopy, CRDs) + if err := ackgenerate.BuildControllerPreCodegen(ctx, pipelineOpts); err != nil { return err } - util.Tracef("template execution: %s\n", time.Since(execStart)) - writeStart := time.Now() - for path, contents := range ts.Executed() { - if optDryRun { - fmt.Printf("============================= %s ======================================\n", path) - fmt.Println(strings.TrimSpace(contents.String())) - continue - } - outPath := filepath.Join(optOutputPath, path) - outDir := filepath.Dir(outPath) - if _, err := sdk.EnsureDir(outDir); err != nil { - return err - } - if err = ioutil.WriteFile(outPath, contents.Bytes(), 0666); err != nil { - return err - } - } - util.Tracef("file writing (%d files): %s\n", len(ts.Executed()), time.Since(writeStart)) - util.Tracef("generateController total: %s\n", time.Since(cmdStart)) - return nil + // Step 3: Post-codegen pipeline (go mod tidy, RBAC, formatting, boilerplate) + return ackgenerate.BuildControllerPostCodegen(pipelineOpts) } diff --git a/cmd/ack-generate/command/crossplane.go b/cmd/ack-generate/command/crossplane.go index 555baa77..1d20782f 100644 --- a/cmd/ack-generate/command/crossplane.go +++ b/cmd/ack-generate/command/crossplane.go @@ -29,6 +29,8 @@ import ( "github.com/aws-controllers-k8s/code-generator/pkg/sdk" ) +var optGenVersion string + // crossplaneCmd is the command that generates Crossplane API types var crossplaneCmd = &cobra.Command{ Use: "crossplane ", @@ -37,6 +39,9 @@ var crossplaneCmd = &cobra.Command{ } func init() { + crossplaneCmd.PersistentFlags().StringVar( + &optGenVersion, "version", "v1alpha1", "the resource API Version to use when generating API infrastructure and type definitions", + ) rootCmd.AddCommand(crossplaneCmd) } diff --git a/cmd/ack-generate/command/olm.go b/cmd/ack-generate/command/olm.go index 631f8e35..526267db 100644 --- a/cmd/ack-generate/command/olm.go +++ b/cmd/ack-generate/command/olm.go @@ -22,6 +22,7 @@ import ( "github.com/ghodss/yaml" "github.com/spf13/cobra" + ackgenerate "github.com/aws-controllers-k8s/code-generator/pkg/generate/ack" olmgenerate "github.com/aws-controllers-k8s/code-generator/pkg/generate/olm" ackmetadata "github.com/aws-controllers-k8s/code-generator/pkg/metadata" "github.com/aws-controllers-k8s/code-generator/pkg/sdk" @@ -82,6 +83,15 @@ func generateOLMAssets(cmd *cobra.Command, args []string) error { version := args[1] + // Resolve config paths from the controller repo when not explicitly provided + resolved := ackgenerate.ResolveConfigPaths(optOutputPath) + if optGeneratorConfigPath == "" { + optGeneratorConfigPath = resolved.GeneratorConfigPath + } + if optMetadataConfigPath == "" { + optMetadataConfigPath = resolved.MetadataConfigPath + } + // Load generator config to resolve model name before fetching cfg, err := setupGenerator(svcAlias) if err != nil { @@ -127,7 +137,7 @@ func generateOLMAssets(cmd *cobra.Command, args []string) error { } // generate templates - ts, err := olmgenerate.BundleAssets(m, commonMeta, svcConf, version, optImageRepository, optTemplateDirs) + ts, err := olmgenerate.BundleAssets(m, commonMeta, svcConf, version, optImageRepository, optTemplateDirs, embeddedTemplatesFS) if err != nil { return err } diff --git a/cmd/ack-generate/command/release.go b/cmd/ack-generate/command/release.go index 3542bfbd..ab5ca713 100644 --- a/cmd/ack-generate/command/release.go +++ b/cmd/ack-generate/command/release.go @@ -14,92 +14,75 @@ package command import ( + "context" "fmt" - "io/ioutil" - "path/filepath" "strings" "github.com/spf13/cobra" ackgenerate "github.com/aws-controllers-k8s/code-generator/pkg/generate/ack" - ackmetadata "github.com/aws-controllers-k8s/code-generator/pkg/metadata" "github.com/aws-controllers-k8s/code-generator/pkg/sdk" ) -var optReleaseOutputPath string - var releaseCmd = &cobra.Command{ Use: "release ", Short: "Generates release artifacts for a specific service controller and release version", - RunE: generateRelease, + Long: `Runs the full release pipeline for a controller repo. By default uses +the current working directory; use -o to specify the controller path. +This includes template generation, CRDs, RBAC, and Helm template +post-processing. + +Usage: + ack-generate release s3 v1.2.3 -o /path/to/s3-controller + # or from within the controller repo: + ack-generate release s3 v1.2.3`, + RunE: generateRelease, } func init() { - releaseCmd.PersistentFlags().StringVarP( - &optReleaseOutputPath, "output", "o", "", "path to root directory to create generated files. Defaults to "+optServicesDir+"/$service", - ) rootCmd.AddCommand(releaseCmd) } -// generateRelease generates the Helm charts and other release artifacts for a -// service controller and release version func generateRelease(cmd *cobra.Command, args []string) error { if len(args) != 2 { return fmt.Errorf("please specify the service alias and the release version to generate release artifacts for") } svcAlias := strings.ToLower(args[0]) - if optReleaseOutputPath == "" { - optReleaseOutputPath = filepath.Join(optServicesDir, svcAlias) - } - if optImageRepository == "" { - optImageRepository = fmt.Sprintf("public.ecr.aws/aws-controllers-k8s/%s-controller", svcAlias) - } - // TODO(jaypipes): We could do some git-fu here to verify that the release - // version supplied hasn't been used (as a Git tag) before... releaseVersion := strings.ToLower(args[1]) - // Load generator config to resolve model name before fetching - cfg, err := setupGenerator(svcAlias) - if err != nil { - return err - } + ctx, cancel := sdk.ContextWithSigterm(context.Background()) + defer cancel() - m, err := loadModel(svcAlias, "", "", cfg) + controllerCtx, err := resolveControllerContext(svcAlias) if err != nil { return err } - metadata, err := ackmetadata.NewServiceMetadata(optMetadataConfigPath) - if err != nil { - return err + if optImageRepository == "" { + optImageRepository = fmt.Sprintf("public.ecr.aws/aws-controllers-k8s/%s-controller", svcAlias) } + fmt.Printf("Building release artifacts for %s-%s\n", svcAlias, releaseVersion) ts, err := ackgenerate.Release( - m, metadata, optTemplateDirs, + controllerCtx.Model, controllerCtx.Metadata, optTemplateDirs, releaseVersion, optImageRepository, optServiceAccountName, + embeddedTemplatesFS, ) if err != nil { return err } - - if err = ts.Execute(); err != nil { + if err := writeTemplateSet(ts, optOutputPath); err != nil { return err } - for path, contents := range ts.Executed() { - if optDryRun { - fmt.Printf("============================= %s ======================================\n", path) - fmt.Println(strings.TrimSpace(contents.String())) - continue - } - outPath := filepath.Join(optReleaseOutputPath, path) - outDir := filepath.Dir(outPath) - if _, err := sdk.EnsureDir(outDir); err != nil { - return err - } - if err = ioutil.WriteFile(outPath, contents.Bytes(), 0666); err != nil { - return err - } + releaseOpts := ackgenerate.BuildReleaseOptions{ + SvcAlias: controllerCtx.SvcAlias, + ControllerSourcePath: controllerCtx.ControllerPath, + APIVersion: controllerCtx.APIVersion, + RBACRoleName: controllerCtx.RBACRoleName, + RuntimeVersion: controllerCtx.RuntimeVersion, + ControllerGenVersion: optControllerGenVersion, + CacheDir: optCacheDir, } - return nil + return ackgenerate.BuildRelease(ctx, releaseOpts) } diff --git a/cmd/ack-generate/command/root.go b/cmd/ack-generate/command/root.go index 5013a6c8..d8b30596 100644 --- a/cmd/ack-generate/command/root.go +++ b/cmd/ack-generate/command/root.go @@ -15,10 +15,13 @@ package command import ( "fmt" + "io/fs" "os" "path/filepath" "github.com/spf13/cobra" + + codegenerator "github.com/aws-controllers-k8s/code-generator" ) const ( @@ -46,6 +49,9 @@ var ( optOutputPath string optServiceAccountName string optImageRepository string + optControllerGenVersion string + embeddedTemplatesFS fs.FS + embeddedBoilerplateFS fs.FS ) var rootCmd = &cobra.Command{ @@ -69,6 +75,14 @@ func init() { } defaultCacheDir = filepath.Join(hd, ".cache", appName) + sub, fsErr := fs.Sub(codegenerator.TemplateFS, "templates") + if fsErr != nil { + fmt.Printf("unable to load embedded templates: %s\n", fsErr) + os.Exit(1) + } + embeddedTemplatesFS = sub + embeddedBoilerplateFS = codegenerator.BoilerplateFiles + // try to determine a default template and services directory. If the call // is executing `ack-generate` via a checked-out ACK source repository, // then the templates and services directory in the source repo can serve @@ -130,6 +144,9 @@ func init() { rootCmd.PersistentFlags().StringVar( &optImageRepository, "image-repository", "", "the Docker image repository to use in release artifacts. Defaults to 'public.ecr.aws/aws-controllers-k8s/$service-controller'", ) + rootCmd.PersistentFlags().StringVar( + &optControllerGenVersion, "controller-gen-version", "v0.19.0", "Required version of controller-gen (sigs.k8s.io/controller-tools)", + ) } // Execute adds all child commands to the root command and sets flags diff --git a/embed.go b/embed.go new file mode 100644 index 00000000..9e642901 --- /dev/null +++ b/embed.go @@ -0,0 +1,22 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package codegenerator + +import "embed" + +//go:embed all:templates +var TemplateFS embed.FS + +//go:embed CODE_OF_CONDUCT.md CONTRIBUTING.md GOVERNANCE.md LICENSE NOTICE +var BoilerplateFiles embed.FS diff --git a/pkg/generate/ack/apis.go b/pkg/generate/ack/apis.go index e471a95a..9a7c1721 100644 --- a/pkg/generate/ack/apis.go +++ b/pkg/generate/ack/apis.go @@ -14,15 +14,11 @@ package ack import ( - "path/filepath" "strings" ttpl "text/template" - "time" "github.com/aws-controllers-k8s/code-generator/pkg/generate/templateset" ackmodel "github.com/aws-controllers-k8s/code-generator/pkg/model" - "github.com/aws-controllers-k8s/code-generator/pkg/util" - "github.com/iancoleman/strcase" ) var ( @@ -37,78 +33,11 @@ var ( "apis/enum_def.go.tpl", "apis/type_def.go.tpl", } - apisCopyPaths = []string{} - apisFuncMap = ttpl.FuncMap{ + apisFuncMap = ttpl.FuncMap{ "Join": strings.Join, } ) -// APIs returns a pointer to a TemplateSet containing all the templates for -// generating ACK service controller's apis/ contents -func APIs( - m *ackmodel.Model, - templateBasePaths []string, -) (*templateset.TemplateSet, error) { - totalStart := time.Now() - - enumStart := time.Now() - enumDefs, err := m.GetEnumDefs() - if err != nil { - return nil, err - } - util.Tracef("GetEnumDefs (%d enums): %s\n", len(enumDefs), time.Since(enumStart)) - - typeStart := time.Now() - typeDefs, err := m.GetTypeDefs() - if err != nil { - return nil, err - } - util.Tracef("GetTypeDefs (%d types): %s\n", len(typeDefs), time.Since(typeStart)) - - crdStart := time.Now() - crds, err := m.GetCRDs() - if err != nil { - return nil, err - } - util.Tracef("GetCRDs (%d CRDs): %s\n", len(crds), time.Since(crdStart)) - - tplStart := time.Now() - ts := templateset.New( - templateBasePaths, - apisIncludePaths, - apisCopyPaths, - apisFuncMap, - ) - - metaVars := m.MetaVars() - apiVars := &templateAPIVars{ - metaVars, - enumDefs, - typeDefs, - } - for _, path := range apisTemplatePaths { - outPath := strings.TrimSuffix(filepath.Base(path), ".tpl") - if err = ts.Add(outPath, path, apiVars); err != nil { - return nil, err - } - } - - for _, crd := range crds { - crdFileName := strcase.ToSnake(crd.Kind) + ".go" - crdVars := &templateCRDVars{ - metaVars, - m.SDKAPI, - crd, - } - if err = ts.Add(crdFileName, "apis/crd.go.tpl", crdVars); err != nil { - return nil, err - } - } - util.Tracef("template setup: %s\n", time.Since(tplStart)) - util.Tracef("APIs() total: %s\n", time.Since(totalStart)) - return ts, nil -} - // templateAPIVars contains template variables for templates that output Go // code in the /services/$SERVICE/apis/$API_VERSION directory type templateAPIVars struct { diff --git a/pkg/generate/ack/controller.go b/pkg/generate/ack/controller.go index 5beab739..82f1c193 100644 --- a/pkg/generate/ack/controller.go +++ b/pkg/generate/ack/controller.go @@ -14,12 +14,15 @@ package ack import ( + "io/fs" "path/filepath" "sort" "strings" ttpl "text/template" "time" + "github.com/iancoleman/strcase" + awssdkmodel "github.com/aws-controllers-k8s/code-generator/pkg/api" ackgenconfig "github.com/aws-controllers-k8s/code-generator/pkg/config" "github.com/aws-controllers-k8s/code-generator/pkg/fieldpath" @@ -230,15 +233,31 @@ var ( ) // Controller returns a pointer to a TemplateSet containing all the templates -// for generating ACK service controller implementations +// for generating ACK service controller implementations, including the +// Kubernetes API types (apis/ directory). func Controller( m *ackmodel.Model, templateBasePaths []string, - // serviceAccountName is the name of the ServiceAccount used in the Helm chart serviceAccountName string, + apiVersion string, + fallbackFS fs.FS, ) (*templateset.TemplateSet, error) { totalStart := time.Now() + enumStart := time.Now() + enumDefs, err := m.GetEnumDefs() + if err != nil { + return nil, err + } + util.Tracef("GetEnumDefs (%d enums): %s\n", len(enumDefs), time.Since(enumStart)) + + typeStart := time.Now() + typeDefs, err := m.GetTypeDefs() + if err != nil { + return nil, err + } + util.Tracef("GetTypeDefs (%d types): %s\n", len(typeDefs), time.Since(typeStart)) + crdStart := time.Now() crds, err := m.GetCRDs() if err != nil { @@ -257,21 +276,60 @@ func Controller( m.SDKAPI, r, } - code, err := ResourceHookCode(templateBasePaths, r, hookID, crdVars, controllerFuncMap) + code, err := ResourceHookCode(templateBasePaths, r, hookID, crdVars, controllerFuncMap, fallbackFS) if err != nil { return "", err } return code, nil } + // Merge include paths from both APIs and controller templates + allIncludePaths := make([]string, 0, len(apisIncludePaths)+len(controllerIncludePaths)) + allIncludePaths = append(allIncludePaths, apisIncludePaths...) + allIncludePaths = append(allIncludePaths, controllerIncludePaths...) + + // Also merge the func maps + mergedFuncMap := ttpl.FuncMap{} + for k, v := range apisFuncMap { + mergedFuncMap[k] = v + } + for k, v := range controllerFuncMap { + mergedFuncMap[k] = v + } + ts := templateset.New( templateBasePaths, - controllerIncludePaths, + allIncludePaths, controllerCopyPaths, - controllerFuncMap, - ) + mergedFuncMap, + ).WithFallbackFS(fallbackFS) + + // --- APIs templates (apis/{apiVersion}/) --- + apiVars := &templateAPIVars{ + metaVars, + enumDefs, + typeDefs, + } + apisPrefix := filepath.Join("apis", apiVersion) + for _, path := range apisTemplatePaths { + outPath := filepath.Join(apisPrefix, strings.TrimSuffix(filepath.Base(path), ".tpl")) + if err = ts.Add(outPath, path, apiVars); err != nil { + return nil, err + } + } + for _, crd := range crds { + crdFileName := strcase.ToSnake(crd.Kind) + ".go" + crdVars := &templateCRDVars{ + metaVars, + m.SDKAPI, + crd, + } + if err = ts.Add(filepath.Join(apisPrefix, crdFileName), "apis/crd.go.tpl", crdVars); err != nil { + return nil, err + } + } - // First add all the CRD pkg/resource templates + // --- Controller templates (pkg/resource/) --- targets := []string{ "delta.go.tpl", "descriptor.go.tpl", @@ -285,7 +343,6 @@ func Controller( } for _, crd := range crds { for _, target := range targets { - // skip adding "tags.go.tpl" file if tagging is ignored for a crd if target == "tags.go.tpl" && crd.Config().TagsAreIgnored(crd.Names.Original) { continue } @@ -311,14 +368,12 @@ func Controller( return nil, err } - // Next add the template for pkg/version/version.go file if err = ts.Add("pkg/version/version.go", "pkg/version/version.go.tpl", nil); err != nil { return nil, err } - // Next add the template for the main.go file + // main.go snakeCasedCRDNames := make([]string, 0) - // using Map to implement the Set referencedServiceNamesMap := make(map[string]struct{}) for _, crd := range crds { snakeCasedCRDNames = append(snakeCasedCRDNames, crd.Names.Snake) @@ -340,14 +395,17 @@ func Controller( return nil, err } - // Finally, add the configuration YAML file templates + if err = ts.Add("Makefile", "Makefile.tpl", metaVars); err != nil { + return nil, err + } + for _, path := range controllerConfigTemplatePaths { outPath := strings.TrimSuffix(path, ".tpl") if err = ts.Add(outPath, path, configVars); err != nil { return nil, err } } - util.Tracef("template setup (%d templates): %s\n", len(ts.Executed())+len(controllerConfigTemplatePaths), time.Since(tplStart)) + util.Tracef("template setup: %s\n", time.Since(tplStart)) util.Tracef("Controller() total: %s\n", time.Since(totalStart)) return ts, nil } diff --git a/pkg/generate/ack/hook.go b/pkg/generate/ack/hook.go index 1ea47915..cc9a5496 100644 --- a/pkg/generate/ack/hook.go +++ b/pkg/generate/ack/hook.go @@ -16,6 +16,7 @@ package ack import ( "bytes" "fmt" + "io/fs" "io/ioutil" "path/filepath" ttpl "text/template" @@ -161,6 +162,7 @@ func ResourceHookCode( hookID string, vars interface{}, funcMap ttpl.FuncMap, + fallbackFS fs.FS, ) (string, error) { resourceName := r.Names.Original if resourceName == "" || hookID == "" { @@ -222,6 +224,28 @@ func ResourceHookCode( } return b.String(), nil } + if fallbackFS != nil { + tplContents, fsErr := fs.ReadFile(fallbackFS, filepath.ToSlash(*hook.TemplatePath)) + if fsErr == nil { + t := ttpl.New(*hook.TemplatePath) + t = t.Funcs(funcMap) + if t, err := t.Parse(string(tplContents)); err != nil { + return "", fmt.Errorf( + "resource %s hook config for %s is invalid: error parsing %s: %s", + resourceName, hookID, *hook.TemplatePath, err, + ) + } else { + var b bytes.Buffer + if err := t.Execute(&b, vars); err != nil { + return "", fmt.Errorf( + "resource %s hook config for %s is invalid: error executing %s: %s", + resourceName, hookID, *hook.TemplatePath, err, + ) + } + return b.String(), nil + } + } + } err := fmt.Errorf( "resource %s hook config for %s is invalid: template_path %s not found", resourceName, hookID, *hook.TemplatePath, diff --git a/pkg/generate/ack/hook_test.go b/pkg/generate/ack/hook_test.go index 1aa02fb3..5524f6ed 100644 --- a/pkg/generate/ack/hook_test.go +++ b/pkg/generate/ack/hook_test.go @@ -38,7 +38,7 @@ func TestResourceHookCodeInline(t *testing.T) { // The Broker's update operation has a special hook callback configured expected := `if err := rm.requeueIfNotRunning(latest); err != nil { return nil, err }` - got, err := ack.ResourceHookCode(basePaths, crd, hookID, nil, nil) + got, err := ack.ResourceHookCode(basePaths, crd, hookID, nil, nil, nil) assert.Nil(err) assert.Equal(expected, got) } @@ -59,7 +59,7 @@ func TestResourceHookCodeTemplatePath(t *testing.T) { // The Broker's delete operation has a special hook configured to point to a template. expected := "// this is my template.\n" - got, err := ack.ResourceHookCode(basePaths, crd, hookID, nil, nil) + got, err := ack.ResourceHookCode(basePaths, crd, hookID, nil, nil, nil) assert.Nil(err) assert.Equal(expected, got) } diff --git a/pkg/generate/ack/pipeline.go b/pkg/generate/ack/pipeline.go new file mode 100644 index 00000000..7b87a533 --- /dev/null +++ b/pkg/generate/ack/pipeline.go @@ -0,0 +1,520 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package ack + +import ( + "bytes" + "context" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/ghodss/yaml" + rbacv1 "k8s.io/api/rbac/v1" + + acksdk "github.com/aws-controllers-k8s/code-generator/pkg/sdk" + "github.com/aws-controllers-k8s/code-generator/pkg/util" +) + +const ( + goimportsPackage = "golang.org/x/tools/cmd/goimports@latest" +) + +// BuildControllerOptions contains all the options needed to run the full +// controller generation pipeline. +type BuildControllerOptions struct { + // SvcAlias is the AWS service alias (e.g., "s3", "ec2") + SvcAlias string + // ControllerSourcePath is the root of the service controller repo + ControllerSourcePath string + // APIVersion is the Kubernetes API version (e.g., "v1alpha1") + APIVersion string + // RBACRoleName is the name of the RBAC ClusterRole + RBACRoleName string + // RuntimeVersion is the runtime module version for CRD generation + RuntimeVersion string + // ControllerGenVersion is the required version of controller-gen + ControllerGenVersion string + // CacheDir is the path to the ack-generate cache directory + CacheDir string + // BoilerplatePath is the path to the boilerplate header file + BoilerplatePath string + // BoilerplateFS is the embedded filesystem containing boilerplate files + BoilerplateFS fs.FS + // TemplatesFS is the embedded filesystem containing templates + TemplatesFS fs.FS +} + +// BuildControllerPreCodegen runs the pipeline steps that must happen before +// code generation: runtime CRDs, deepcopy, and CRD manifests. +func BuildControllerPreCodegen(ctx context.Context, opts BuildControllerOptions) error { + if err := checkControllerGen(opts.ControllerGenVersion); err != nil { + return err + } + + configDir := filepath.Join(opts.ControllerSourcePath, "config") + apisDir := filepath.Join(opts.ControllerSourcePath, "apis", opts.APIVersion) + + boilerplatePath := opts.BoilerplatePath + if boilerplatePath == "" { + svcBoilerplate := filepath.Join(opts.ControllerSourcePath, "templates", "boilerplate.txt") + if util.FileExists(svcBoilerplate) { + boilerplatePath = svcBoilerplate + } else if opts.TemplatesFS != nil { + data, err := fs.ReadFile(opts.TemplatesFS, "boilerplate.txt") + if err != nil { + return fmt.Errorf("reading embedded boilerplate.txt: %w", err) + } + tmpFile, err := os.CreateTemp("", "boilerplate-*.txt") + if err != nil { + return fmt.Errorf("creating temp boilerplate file: %w", err) + } + defer os.Remove(tmpFile.Name()) + if _, err := tmpFile.Write(data); err != nil { + tmpFile.Close() + return fmt.Errorf("writing temp boilerplate file: %w", err) + } + tmpFile.Close() + boilerplatePath = tmpFile.Name() + } + } + + // Step 1: Copy runtime CRD config (preserves bases/ + kustomization.yaml) + fmt.Println("Copying common custom resource definitions") + commonCRDDir := filepath.Join(configDir, "crd", "common") + if err := os.MkdirAll(commonCRDDir, 0755); err != nil { + return fmt.Errorf("creating common CRD directory: %w", err) + } + if err := copyRuntimeCRDConfig(ctx, opts.CacheDir, opts.RuntimeVersion, commonCRDDir); err != nil { + return fmt.Errorf("copying runtime CRD config: %w", err) + } + + // Step 2: controller-gen object (deepcopy) + fmt.Printf("Generating deepcopy code for %s\n", opts.SvcAlias) + objectArgs := []string{"object:headerFile=" + boilerplatePath, "paths=./..."} + if err := runControllerGen(objectArgs, apisDir); err != nil { + return fmt.Errorf("generating deepcopy code: %w", err) + } + + // Step 3: controller-gen crd + fmt.Printf("Generating custom resource definitions for %s\n", opts.SvcAlias) + crdArgs := []string{ + "crd:allowDangerousTypes=true", + "paths=./...", + "output:crd:artifacts:config=" + filepath.Join(configDir, "crd", "bases"), + } + if err := runControllerGen(crdArgs, apisDir); err != nil { + return fmt.Errorf("generating CRDs: %w", err) + } + + return nil +} + +// BuildControllerPostCodegen runs the pipeline steps that happen after code +// generation: go mod tidy, RBAC, formatting, and boilerplate copying. +func BuildControllerPostCodegen(opts BuildControllerOptions) error { + configDir := filepath.Join(opts.ControllerSourcePath, "config") + resourceDir := filepath.Join(opts.ControllerSourcePath, "pkg", "resource") + + // Step 1: go mod tidy + fmt.Println("Running go mod tidy") + if err := runCommand("go", []string{"mod", "tidy"}, opts.ControllerSourcePath); err != nil { + return fmt.Errorf("running go mod tidy: %w", err) + } + + // Step 2: controller-gen rbac + fmt.Printf("Generating RBAC manifests for %s\n", opts.SvcAlias) + rbacArgs := []string{ + "rbac:roleName=" + opts.RBACRoleName, + "paths=./...", + "output:rbac:artifacts:config=" + filepath.Join(configDir, "rbac"), + } + if err := runControllerGen(rbacArgs, resourceDir); err != nil { + return fmt.Errorf("generating RBAC manifests: %w", err) + } + + // Step 3: rename role.yaml → cluster-role-controller.yaml + roleYAML := filepath.Join(configDir, "rbac", "role.yaml") + clusterRoleYAML := filepath.Join(configDir, "rbac", "cluster-role-controller.yaml") + if err := os.Rename(roleYAML, clusterRoleYAML); err != nil { + return fmt.Errorf("renaming role.yaml: %w", err) + } + + // Step 4: copy namespaced overlay patches from embedded FS + if opts.TemplatesFS != nil { + fmt.Println("Copying namespaced overlay patches") + if err := copyNamespacedOverlays(opts.TemplatesFS, configDir); err != nil { + return fmt.Errorf("copying namespaced overlays: %w", err) + } + } + + // Step 5: gofmt + goimports + fmt.Printf("Running formatters against generated code for %s\n", opts.SvcAlias) + if err := runFormatters(opts.ControllerSourcePath); err != nil { + return fmt.Errorf("running formatters: %w", err) + } + + // Step 6: copy boilerplate files + if opts.BoilerplateFS != nil { + fmt.Println("Updating repository maintenance files") + if err := copyBoilerplate(opts.BoilerplateFS, opts.ControllerSourcePath); err != nil { + return fmt.Errorf("copying boilerplate files: %w", err) + } + } + + return nil +} + +// BuildReleaseOptions contains all the options needed to run the full +// release artifact generation pipeline. +type BuildReleaseOptions struct { + // SvcAlias is the AWS service alias (e.g., "s3", "ec2") + SvcAlias string + // ControllerSourcePath is the root of the service controller repo + ControllerSourcePath string + // APIVersion is the Kubernetes API version (e.g., "v1alpha1") + APIVersion string + // RBACRoleName is the name of the RBAC ClusterRole + RBACRoleName string + // RuntimeVersion is the runtime module version for CRD generation + RuntimeVersion string + // ControllerGenVersion is the required version of controller-gen + ControllerGenVersion string + // CacheDir is the path to the ack-generate cache directory + CacheDir string +} + +// BuildRelease runs the post-processing steps for release artifact generation: +// 1. Copy runtime CRDs → helm/crds +// 2. controller-gen crd (service CRDs → helm/crds) +// 3. controller-gen rbac (→ helm/templates) +// 4. RBAC rules injection into _helpers.tpl +func BuildRelease(ctx context.Context, opts BuildReleaseOptions) error { + if err := checkControllerGen(opts.ControllerGenVersion); err != nil { + return err + } + + helmDir := filepath.Join(opts.ControllerSourcePath, "helm") + apisDir := filepath.Join(opts.ControllerSourcePath, "apis", opts.APIVersion) + resourceDir := filepath.Join(opts.ControllerSourcePath, "pkg", "resource") + + // Step 1: Copy runtime CRDs → helm/crds + fmt.Println("Copying common custom resource definitions") + crdsDir := filepath.Join(helmDir, "crds") + if err := copyRuntimeCRDBases(ctx, opts.CacheDir, opts.RuntimeVersion, crdsDir); err != nil { + return fmt.Errorf("copying runtime CRDs for helm: %w", err) + } + + // Step 2: controller-gen crd (service CRDs → helm/crds) + fmt.Printf("Generating custom resource definitions for %s\n", opts.SvcAlias) + crdArgs := []string{ + "crd:allowDangerousTypes=true", + "paths=./...", + "output:crd:artifacts:config=" + crdsDir, + } + if err := runControllerGen(crdArgs, apisDir); err != nil { + return fmt.Errorf("generating service CRDs for helm: %w", err) + } + + // Step 3: controller-gen rbac (→ helm/templates) + fmt.Printf("Generating RBAC manifests for %s\n", opts.SvcAlias) + rbacArgs := []string{ + "rbac:roleName=" + opts.RBACRoleName, + "paths=./...", + "output:rbac:artifacts:config=" + filepath.Join(helmDir, "templates"), + } + if err := runControllerGen(rbacArgs, resourceDir); err != nil { + return fmt.Errorf("generating RBAC manifests for helm: %w", err) + } + + // Step 4: Inject RBAC rules into _helpers.tpl + fmt.Println("Injecting RBAC rules into Helm templates") + if err := injectRBACRules(helmDir); err != nil { + return fmt.Errorf("injecting RBAC rules: %w", err) + } + + return nil +} + +// copyRuntimeCRDConfig fetches the runtime CRD config files from GitHub +// (cached locally) and copies them to the controller's config/crd/common/ +// directory, preserving the directory structure (bases/ + kustomization.yaml). +func copyRuntimeCRDConfig(ctx context.Context, cacheDir string, runtimeVersion string, destDir string) error { + cachedDir, err := acksdk.EnsureRuntimeCRDs(ctx, cacheDir, runtimeVersion) + if err != nil { + return err + } + + for _, relPath := range acksdk.RuntimeCRDFiles() { + srcPath := filepath.Join(cachedDir, relPath) + destPath := filepath.Join(destDir, relPath) + if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + return err + } + data, err := os.ReadFile(srcPath) + if err != nil { + return err + } + if err := os.WriteFile(destPath, data, 0666); err != nil { + return err + } + } + return nil +} + +// copyRuntimeCRDBases fetches the runtime CRD files and copies only the +// bases/*.yaml files to the destination (used for helm/crds/ which needs +// flat CRD files without kustomization). +func copyRuntimeCRDBases(ctx context.Context, cacheDir string, runtimeVersion string, destDir string) error { + cachedDir, err := acksdk.EnsureRuntimeCRDs(ctx, cacheDir, runtimeVersion) + if err != nil { + return err + } + + if err := os.MkdirAll(destDir, 0755); err != nil { + return err + } + + for _, relPath := range acksdk.RuntimeCRDFiles() { + if !strings.HasPrefix(relPath, "bases/") { + continue + } + srcPath := filepath.Join(cachedDir, relPath) + destPath := filepath.Join(destDir, filepath.Base(relPath)) + data, err := os.ReadFile(srcPath) + if err != nil { + return err + } + if err := os.WriteFile(destPath, data, 0666); err != nil { + return err + } + } + return nil +} + +// checkControllerGen verifies that controller-gen is installed and at the +// required version. If missing or at the wrong version, it installs the +// correct version via `go install`. +func checkControllerGen(requiredVersion string) error { + pkg := "sigs.k8s.io/controller-tools/cmd/controller-gen@" + requiredVersion + + path, err := exec.LookPath("controller-gen") + if err != nil { + fmt.Printf("controller-gen not found, installing %s...\n", requiredVersion) + return installControllerGen(pkg) + } + + out, err := exec.Command(path, "--version").Output() + if err != nil { + fmt.Printf("Unable to determine controller-gen version, reinstalling %s...\n", requiredVersion) + return installControllerGen(pkg) + } + version := strings.TrimSpace(string(out)) + if !strings.Contains(version, requiredVersion) { + fmt.Printf("controller-gen version mismatch: have %q, need %s. Installing...\n", version, requiredVersion) + return installControllerGen(pkg) + } + return nil +} + +func installControllerGen(pkg string) error { + cmd := exec.Command("go", "install", pkg) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("installing controller-gen: %w", err) + } + return nil +} + +// ensureGoimports checks if goimports is installed and installs it if not. +func ensureGoimports() error { + if _, err := exec.LookPath("goimports"); err == nil { + return nil + } + fmt.Println("Installing goimports...") + cmd := exec.Command("go", "install", goimportsPackage) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// runControllerGen executes controller-gen with the given arguments in the +// specified directory. +func runControllerGen(args []string, dir string) error { + return runCommand("controller-gen", args, dir) +} + +// runCommand executes a command with the given arguments in the specified +// directory, forwarding stdout/stderr. +func runCommand(name string, args []string, dir string) error { + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// runFormatters runs gofmt and goimports on the given directory. +func runFormatters(dir string) error { + if err := runCommand("gofmt", []string{"-w", dir}, ""); err != nil { + return fmt.Errorf("running gofmt: %w", err) + } + + if err := ensureGoimports(); err != nil { + return fmt.Errorf("ensuring goimports is installed: %w", err) + } + + if err := runCommand("goimports", []string{"-w", dir}, ""); err != nil { + return fmt.Errorf("running goimports: %w", err) + } + return nil +} + +// injectRBACRules reads the controller-gen-generated role.yaml, extracts the +// RBAC rules, and injects them into _helpers.tpl replacing the +// SEDREPLACERULES marker. +func injectRBACRules(helmDir string) error { + roleYAMLPath := filepath.Join(helmDir, "templates", "role.yaml") + helpersPath := filepath.Join(helmDir, "templates", "_helpers.tpl") + + roleData, err := os.ReadFile(roleYAMLPath) + if err != nil { + return fmt.Errorf("reading role.yaml: %w", err) + } + + var role rbacv1.ClusterRole + if err := yaml.Unmarshal(roleData, &role); err != nil { + return fmt.Errorf("unmarshaling role.yaml: %w", err) + } + + // Marshal just the rules back to YAML + rulesYAML, err := yaml.Marshal(role.Rules) + if err != nil { + return fmt.Errorf("marshaling RBAC rules: %w", err) + } + + // The rules need to be formatted as they appear in a ClusterRole spec, + // with "rules:" prefix + var rulesBlock bytes.Buffer + rulesBlock.WriteString("rules:\n") + for _, line := range strings.Split(strings.TrimSpace(string(rulesYAML)), "\n") { + rulesBlock.WriteString(line) + rulesBlock.WriteString("\n") + } + + helpersData, err := os.ReadFile(helpersPath) + if err != nil { + return fmt.Errorf("reading _helpers.tpl: %w", err) + } + + helpersStr := string(helpersData) + result := strings.Replace( + helpersStr, + "SEDREPLACERULES", + strings.TrimSpace(rulesBlock.String()), + 1, + ) + if result == helpersStr { + return fmt.Errorf("SEDREPLACERULES marker not found in %s; ensure _helpers.tpl contains the marker", helpersPath) + } + + if err := os.WriteFile(helpersPath, []byte(result), 0666); err != nil { + return fmt.Errorf("writing _helpers.tpl: %w", err) + } + + // Remove the original role.yaml + return os.Remove(roleYAMLPath) +} + +// copyBoilerplate writes embedded boilerplate files (LICENSE, CONTRIBUTING.md, +// etc.) to the controller repo. +func copyBoilerplate(fsys fs.FS, destDir string) error { + files := []string{ + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "GOVERNANCE.md", + "LICENSE", + "NOTICE", + } + for _, name := range files { + data, err := fs.ReadFile(fsys, name) + if err != nil { + return fmt.Errorf("reading embedded %s: %w", name, err) + } + destPath := filepath.Join(destDir, name) + if err := os.WriteFile(destPath, data, 0666); err != nil { + return fmt.Errorf("writing %s: %w", name, err) + } + } + return nil +} + +// copyNamespacedOverlays copies the namespaced overlay JSON patches from +// the embedded templates FS to the controller's config directory. +func copyNamespacedOverlays(fsys fs.FS, configDir string) error { + overlayDir := filepath.Join(configDir, "overlays", "namespaced") + if err := os.MkdirAll(overlayDir, 0755); err != nil { + return err + } + + files := []string{ + "role-binding.json", + "role.json", + } + for _, name := range files { + srcPath := filepath.Join("config", "overlays", "namespaced", name) + data, err := fs.ReadFile(fsys, filepath.ToSlash(srcPath)) + if err != nil { + return fmt.Errorf("reading embedded %s: %w", srcPath, err) + } + destPath := filepath.Join(overlayDir, name) + if err := os.WriteFile(destPath, data, 0666); err != nil { + return fmt.Errorf("writing %s: %w", name, err) + } + } + return nil +} + +// ResolveConfigPaths checks for config files at the controller source path +// and returns the resolved paths. Only returns paths for files that exist. +type ResolvedConfigPaths struct { + GeneratorConfigPath string + MetadataConfigPath string + DocumentationConfigPath string +} + +// ResolveConfigPaths checks for config files at the controller source path, +// returning paths only for files that exist on disk. +func ResolveConfigPaths(controllerSourcePath string) ResolvedConfigPaths { + resolved := ResolvedConfigPaths{} + candidates := []struct { + filename string + target *string + }{ + {"generator.yaml", &resolved.GeneratorConfigPath}, + {"metadata.yaml", &resolved.MetadataConfigPath}, + {"documentation.yaml", &resolved.DocumentationConfigPath}, + } + for _, c := range candidates { + path := filepath.Join(controllerSourcePath, c.filename) + if util.FileExists(path) { + *c.target = path + } + } + return resolved +} diff --git a/pkg/generate/ack/release.go b/pkg/generate/ack/release.go index 1feb7731..678633b3 100644 --- a/pkg/generate/ack/release.go +++ b/pkg/generate/ack/release.go @@ -15,6 +15,7 @@ package ack import ( "fmt" + "io/fs" "strings" ttpl "text/template" @@ -96,13 +97,14 @@ func Release( imageRepository string, // serviceAccountName is the name of the ServiceAccount used in the Helm chart serviceAccountName string, + fallbackFS fs.FS, ) (*templateset.TemplateSet, error) { ts := templateset.New( templateBasePaths, releaseIncludePaths, releaseCopyPaths, releaseFuncMap(m.MetaVars().ControllerName), - ) + ).WithFallbackFS(fallbackFS) metaVars := m.MetaVars() // Using GetCRDs() directly gives us the proper CamelCase format // that matches the Kubernetes API resource kinds. The previous approach using diff --git a/pkg/generate/olm/olm.go b/pkg/generate/olm/olm.go index 8054329e..f13485e7 100644 --- a/pkg/generate/olm/olm.go +++ b/pkg/generate/olm/olm.go @@ -2,6 +2,7 @@ package olm import ( "fmt" + "io/fs" "strings" ttpl "text/template" "time" @@ -45,6 +46,7 @@ func BundleAssets( releaseVersion string, imageRepository string, templateBasePaths []string, + fallbackFS fs.FS, ) (*templateset.TemplateSet, error) { ts := templateset.New( @@ -52,7 +54,7 @@ func BundleAssets( csvIncludePaths, csvCopyPaths, csvFuncMap, - ) + ).WithFallbackFS(fallbackFS) crds, err := m.GetCRDs() if err != nil { diff --git a/pkg/generate/templateset/templateset.go b/pkg/generate/templateset/templateset.go index 40f35b9a..6aa08a31 100644 --- a/pkg/generate/templateset/templateset.go +++ b/pkg/generate/templateset/templateset.go @@ -16,7 +16,7 @@ package templateset import ( "bytes" "fmt" - "io/ioutil" + "io/fs" "os" "path/filepath" ttpl "text/template" @@ -57,6 +57,9 @@ type TemplateSet struct { templates map[string]templateWithVars funcMap ttpl.FuncMap executed map[string]*bytes.Buffer + // fallbackFS is an optional embedded filesystem used when a template is + // not found in any of the baseSearchPaths on disk. + fallbackFS fs.FS } // New returns a pointer to a TemplateSet @@ -76,30 +79,24 @@ func New( } } +// WithFallbackFS sets an embedded filesystem to use as a fallback when +// templates are not found on disk. Returns the TemplateSet for chaining. +func (ts *TemplateSet) WithFallbackFS(fsys fs.FS) *TemplateSet { + ts.fallbackFS = fsys + return ts +} + // Add constructs a named template from a path and variables func (ts *TemplateSet) Add( outPath string, templatePath string, vars interface{}, ) error { - var foundPath string - for _, basePath := range ts.baseSearchPaths { - path := filepath.Join(basePath, templatePath) - if ackutil.FileExists(path) { - foundPath = path - break - } - } - - if foundPath == "" { - return errTemplateNotFound(templatePath) - } - - tplContents, err := ioutil.ReadFile(foundPath) + tplContents, tplName, err := ts.readTemplate(templatePath) if err != nil { return err } - t := ttpl.New(foundPath) + t := ttpl.New(tplName) t = t.Funcs(ts.funcMap) t, err = t.Parse(string(tplContents)) if err != nil { @@ -112,20 +109,59 @@ func (ts *TemplateSet) Add( return nil } +// readTemplate searches for a template file first on disk (baseSearchPaths), +// then in the fallback embedded FS. Returns the file contents, a name for the +// template, and any error. +func (ts *TemplateSet) readTemplate(templatePath string) ([]byte, string, error) { + for _, basePath := range ts.baseSearchPaths { + path := filepath.Join(basePath, templatePath) + if ackutil.FileExists(path) { + contents, err := os.ReadFile(path) + if err != nil { + return nil, "", err + } + return contents, path, nil + } + } + if ts.fallbackFS != nil { + contents, err := fs.ReadFile(ts.fallbackFS, filepath.ToSlash(templatePath)) + if err == nil { + return contents, templatePath, nil + } + } + return nil, "", errTemplateNotFound(templatePath) +} + // joinIncludes adds all include templates to the supplied template func (ts *TemplateSet) joinIncludes(t *ttpl.Template) error { var err error + found := make(map[string]bool) for _, basePath := range ts.baseSearchPaths { for _, includePath := range ts.includePaths { tplPath := filepath.Join(basePath, includePath) if !ackutil.FileExists(tplPath) { continue } + found[includePath] = true if t, err = includeTemplate(t, tplPath); err != nil { return err } } } + if ts.fallbackFS != nil { + for _, includePath := range ts.includePaths { + if found[includePath] { + continue + } + contents, err := fs.ReadFile(ts.fallbackFS, filepath.ToSlash(includePath)) + if err != nil { + continue + } + if t, err = t.Parse(string(contents)); err != nil { + return err + } + } + } return nil } @@ -154,6 +190,18 @@ func (ts *TemplateSet) Execute() error { ts.executed[path] = b } } + if ts.fallbackFS != nil { + for _, path := range ts.copyPaths { + if _, exists := ts.executed[path]; exists { + continue + } + data, err := fs.ReadFile(ts.fallbackFS, filepath.ToSlash(path)) + if err != nil { + continue + } + ts.executed[path] = bytes.NewBuffer(data) + } + } return nil } @@ -187,7 +235,7 @@ func byteBufferFromFile(path string) (*bytes.Buffer, error) { // includeTemplate includes a template into a supplied Template struct func includeTemplate(t *ttpl.Template, tplPath string) (*ttpl.Template, error) { - tplContents, err := ioutil.ReadFile(tplPath) + tplContents, err := os.ReadFile(tplPath) if err != nil { return nil, err } diff --git a/pkg/metadata/generation_metadata.go b/pkg/metadata/generation_metadata.go index 5d6dc7f1..f68dd332 100644 --- a/pkg/metadata/generation_metadata.go +++ b/pkg/metadata/generation_metadata.go @@ -86,6 +86,21 @@ type lastModificationInfo struct { Reason UpdateReason `json:"reason"` } +// ReadGenerationMetadata reads the ack-generate-metadata.yaml file from the +// given API version directory and returns the parsed metadata. +func ReadGenerationMetadata(apisPath string, apiVersion string) (*GenerationMetadata, error) { + metaPath := filepath.Join(apisPath, apiVersion, outputFileName) + data, err := os.ReadFile(metaPath) + if err != nil { + return nil, err + } + var meta GenerationMetadata + if err := yaml.Unmarshal(data, &meta); err != nil { + return nil, err + } + return &meta, nil +} + // CreateGenerationMetadata gathers information about the generated code and save // a yaml version in the API version directory func CreateGenerationMetadata( diff --git a/pkg/sdk/repo.go b/pkg/sdk/repo.go index 73494ef5..1820b0dd 100644 --- a/pkg/sdk/repo.go +++ b/pkg/sdk/repo.go @@ -75,13 +75,12 @@ func EnsureDir(fp string) (bool, error) { // isDirWriteable returns true if the supplied directory path is writeable, // false otherwise func isDirWriteable(fp string) bool { - testPath := filepath.Join(fp, "test") - f, err := os.Create(testPath) + f, err := os.CreateTemp(fp, ".ack-writeable-check-*") if err != nil { return false } f.Close() - os.Remove(testPath) + os.Remove(f.Name()) return true } diff --git a/pkg/sdk/runtime.go b/pkg/sdk/runtime.go new file mode 100644 index 00000000..239fc6aa --- /dev/null +++ b/pkg/sdk/runtime.go @@ -0,0 +1,162 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package sdk + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + + "golang.org/x/mod/modfile" + + "github.com/aws-controllers-k8s/code-generator/pkg/util" +) + +const ( + runtimeCRDURLTemplate = "https://raw.githubusercontent.com/aws-controllers-k8s/runtime/%s/config/crd/%s" +) + +// runtimeCRDFiles is the set of files we need from the runtime config/crd/ +// directory. The keys are relative paths within config/crd/. +var runtimeCRDFiles = []string{ + "kustomization.yaml", + "bases/services.k8s.aws_fieldexports.yaml", + "bases/services.k8s.aws_iamroleselectors.yaml", +} + +// EnsureRuntimeCRDs ensures that we have a locally-cached copy of the runtime +// CRD config files for a given runtime version. If the files are already +// cached, it returns immediately. Otherwise, it fetches them from GitHub. +// +// The returned string is the path to the cached config/crd/ directory. +func EnsureRuntimeCRDs( + ctx context.Context, + cacheDir string, + runtimeVersion string, +) (string, error) { + crdDir := filepath.Join(cacheDir, "runtime", runtimeVersion, "config", "crd") + + // Check if all files already exist (cache hit) + allCached := true + for _, relPath := range runtimeCRDFiles { + if _, err := os.Stat(filepath.Join(crdDir, relPath)); err != nil { + allCached = false + break + } + } + if allCached { + util.Tracef("EnsureRuntimeCRDs: cache hit for runtime@%s\n", runtimeVersion) + return crdDir, nil + } + + // Fetch missing files from GitHub + for _, relPath := range runtimeCRDFiles { + destPath := filepath.Join(crdDir, relPath) + if _, err := os.Stat(destPath); err == nil { + continue + } + + if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil { + return "", fmt.Errorf("cannot create runtime CRD cache directory: %v", err) + } + + url := fmt.Sprintf(runtimeCRDURLTemplate, runtimeVersion, relPath) + util.Tracef("EnsureRuntimeCRDs: fetching %s\n", url) + + if err := fetchFile(ctx, url, destPath); err != nil { + return "", fmt.Errorf("fetching runtime CRD file %s: %w", relPath, err) + } + } + + util.Tracef("EnsureRuntimeCRDs: cached runtime@%s CRDs\n", runtimeVersion) + return crdDir, nil +} + +// fetchFile downloads a URL and writes it atomically to destPath. +func fetchFile(ctx context.Context, url string, destPath string) error { + ctx, cancel := context.WithTimeout(ctx, defaultHTTPFetchTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("cannot create HTTP request: %v", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("cannot fetch %s: %v", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to fetch %s: HTTP %d", url, resp.StatusCode) + } + + tmpFile, err := os.CreateTemp(filepath.Dir(destPath), ".tmp-*") + if err != nil { + return fmt.Errorf("cannot create temp file: %v", err) + } + tmpPath := tmpFile.Name() + + _, err = io.Copy(tmpFile, resp.Body) + closeErr := tmpFile.Close() + if err != nil { + os.Remove(tmpPath) + return fmt.Errorf("cannot write file: %v", err) + } + if closeErr != nil { + os.Remove(tmpPath) + return fmt.Errorf("cannot close temp file: %v", closeErr) + } + + if err := os.Rename(tmpPath, destPath); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("cannot rename temp file to cache path: %v", err) + } + + return nil +} + +// GetRuntimeVersion returns the runtime module version from a controller's +// go.mod file. +func GetRuntimeVersion(controllerRepoPath string) (string, error) { + goModPath := filepath.Join(controllerRepoPath, "go.mod") + b, err := os.ReadFile(goModPath) + if err != nil { + return "", fmt.Errorf("reading go.mod: %w", err) + } + + goMod, err := modfile.Parse("", b, nil) + if err != nil { + return "", fmt.Errorf("parsing go.mod: %w", err) + } + + const runtimeModule = "github.com/aws-controllers-k8s/runtime" + for _, require := range goMod.Require { + if require.Mod.Path == runtimeModule { + return require.Mod.Version, nil + } + } + return "", fmt.Errorf("runtime module not found in %s", goModPath) +} + +// RuntimeCRDFiles returns the list of runtime CRD files that are cached. +// This is exported for use in the pipeline logic that copies these files +// to the controller's config/crd/common/ directory. +func RuntimeCRDFiles() []string { + return runtimeCRDFiles +} diff --git a/pkg/version/version.go b/pkg/version/version.go index 508adfd2..42f9e935 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -16,6 +16,8 @@ package version import ( "fmt" "runtime" + "runtime/debug" + "strings" ) var ( @@ -31,4 +33,65 @@ var ( func init() { GoVersion = fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH) + + info, ok := debug.ReadBuildInfo() + if !ok { + return + } + + var revision string + var dirty bool + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + revision = s.Value + case "vcs.modified": + dirty = s.Value == "true" + } + } + + if BuildHash == "No Git-hash Provided." && revision != "" { + BuildHash = revision + } + + // When installed via `go install ...@version`, ldflags aren't set. + // Build a version string from the embedded module/VCS info. + if Version == "(Unknown Version)" { + Version = versionFromBuildInfo(info.Main.Version, revision, dirty) + } +} + +func versionFromBuildInfo(moduleVersion string, revision string, dirty bool) string { + // Remote install (go install @v0.58.1): clean tag, no VCS info. + if revision == "" { + if moduleVersion != "" && moduleVersion != "(devel)" { + return moduleVersion + } + return "v0.0.0-dev" + } + + // Local build: construct git-describe-style from VCS info. + short := revision + if len(short) > 7 { + short = short[:7] + } + + // Extract the semver base from a pseudo-version. + // e.g. "v0.58.2-0.20260515001229-e6c811834c1d+dirty" → "v0.58.2" + base := moduleVersion + if i := strings.Index(base, "+"); i >= 0 { + base = base[:i] + } + if parts := strings.SplitN(base, "-", 2); len(parts) > 0 && strings.HasPrefix(parts[0], "v") { + base = parts[0] + } + if base == "" || base == "(devel)" { + base = "v0.0.0" + } + + v := fmt.Sprintf("%s-g%s", base, short) + if dirty { + v += "+dirty" + } + return v } diff --git a/scripts/build-controller-release.sh b/scripts/build-controller-release.sh deleted file mode 100755 index 4cb0cee9..00000000 --- a/scripts/build-controller-release.sh +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env bash - -# A script that builds release artifacts for a single ACK service controller -# for an AWS service API - -set -eo pipefail - -SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -ROOT_DIR="$SCRIPTS_DIR/.." -ACK_GENERATE_OLM=${ACK_GENERATE_OLM:-"false"} - -source "$SCRIPTS_DIR/lib/common.sh" - -check_is_installed controller-gen "You can install controller-gen with the helper scripts/install-controller-gen.sh" -check_is_installed helm "You can install Helm with the helper scripts/install-helm.sh" - -if [[ $ACK_GENERATE_OLM == "true" ]]; then - check_is_installed operator-sdk "You can install Operator SDK with the helper scripts/install-operator-sdk.sh" -fi - -if ! k8s_controller_gen_version_equals "$CONTROLLER_TOOLS_VERSION"; then - echo "FATAL: Existing version of controller-gen \"$(controller-gen --version)\", required version is $CONTROLLER_TOOLS_VERSION." - echo "FATAL: Please uninstall controller-gen and install the required version with scripts/install-controller-gen.sh." - exit 1 -fi - -if ! helm_version_equals_or_greater "$HELM_VERSION"; then - echo "FATAL: Existing version of helm \"$(helm version --template='Version: {{.Version}}')\", required version is $HELM_VERSION." - echo "FATAL: Please update helm, or uninstall helm and install the required version with scripts/install-helm.sh." - exit 1 -fi - -ACK_GENERATE_CACHE_DIR=${ACK_GENERATE_CACHE_DIR:-"$HOME/.cache/aws-controllers-k8s"} -# The ack-generate code generator is in a separate source code repository, -# typically at $GOPATH/src/github.com/aws-controllers-k8s/code-generator -DEFAULT_ACK_GENERATE_BIN_PATH="$ROOT_DIR/../../aws-controllers-k8s/code-generator/bin/ack-generate" -ACK_GENERATE_BIN_PATH=${ACK_GENERATE_BIN_PATH:-$DEFAULT_ACK_GENERATE_BIN_PATH} -ACK_GENERATE_API_VERSION=${ACK_GENERATE_API_VERSION:-"v1alpha1"} -ACK_GENERATE_CONFIG_PATH=${ACK_GENERATE_CONFIG_PATH:-""} -ACK_METADATA_CONFIG_PATH=${ACK_METADATA_CONFIG_PATH:-""} -AWS_SDK_GO_VERSION=${AWS_SDK_GO_VERSION:-""} - -DEFAULT_TEMPLATES_DIR="$ROOT_DIR/../../aws-controllers-k8s/code-generator/templates" -TEMPLATES_DIR=${TEMPLATES_DIR:-$DEFAULT_TEMPLATES_DIR} - -DEFAULT_RUNTIME_DIR="$ROOT_DIR/../runtime" -RUNTIME_DIR=${RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR} -RUNTIME_API_VERSION=${RUNTIME_API_VERSION:-"v1alpha1"} -NON_RELEASE_VERSION="v0.0.0-non-release-version" - -USAGE=" -Usage: - $(basename "$0") - - should be an AWS service API aliases that you wish to build -- e.g. -'s3' 'sns' or 'sqs' - -Environment variables: - ACK_GENERATE_CACHE_DIR Overrides the directory used for caching - AWS API models used by the ack-generate - tool. - Default: $ACK_GENERATE_CACHE_DIR - ACK_GENERATE_BIN_PATH: Overrides the path to the the ack-generate - binary. - Default: $ACK_GENERATE_BIN_PATH - SERVICE_CONTROLLER_SOURCE_PATH: Path to the service controller source code - repository. - Default: ../{SERVICE}-controller - ACK_GENERATE_OLM: Enable Operator Lifecycle Manager generators. - Default: false - ACK_GENERATE_OLMCONFIG_PATH: Path to the service OLM configuration file. Ignored - if ACK_GENERATE_OLM is not true. - Default: {SERVICE_CONTROLLER_SOURCE_PATH}/olm/olmconfig.yaml - ACK_GENERATE_CONFIG_PATH: Specify a path to the generator config YAML file to - instruct the code generator for the service. - Default: {SERVICE_CONTROLLER_SOURCE_PATH}/generator.yaml - ACK_METADATA_CONFIG_PATH: Specify a path to the metadata config YAML file to - instruct the code generator for the service. - Default: {SERVICE_CONTROLLER_SOURCE_PATH}/metadata.yaml - ACK_DOCUMENTATION_CONFIG_PATH: Specify a path to the documentation config YAML file to - instruct the code generator for the service. - Default: {SERVICE_CONTROLLER_SOURCE_PATH}/documentation.yaml - ACK_GENERATE_OUTPUT_PATH: Specify a path for the generator to output - to. - Default: services/{SERVICE} - ACK_GENERATE_IMAGE_REPOSITORY: Specify a Docker image repository to use - for release artifacts - Default: public.ecr.aws/aws-controllers-k8s/{SERVICE}-controller - ACK_GENERATE_SERVICE_ACCOUNT_NAME: Name of the Kubernetes Service Account and - Cluster Role to use in Helm chart. - Default: ack-{SERVICE}-controller - AWS_SDK_GO_VERSION: Overrides the version of github.com/aws/aws-sdk-go used - by 'ack-generate' to fetch the service API Specifications. - Default: Version of aws/aws-sdk-go in service go.mod - K8S_RBAC_ROLE_NAME: Name of the Kubernetes Role to use when - generating the RBAC manifests for the - custom resource definitions. - Default: ack-{SERVICE}-controller - RELEASE_VERSION: SemVer version tag for the release. - Default: v0.0.0-non-release-version -" -if [ $# -ne 1 ]; then - echo "ERROR: $(basename "$0") accepts only one required parameter, the SERVICE" 1>&2 - echo "$USAGE" - exit 1 -fi - -if [ ! -f "$ACK_GENERATE_BIN_PATH" ]; then - if is_installed "ack-generate"; then - ACK_GENERATE_BIN_PATH=$(which "ack-generate") - else - echo "ERROR: Unable to find an ack-generate binary. -Either set the ACK_GENERATE_BIN_PATH to a valid location or -run: - - make build-ack-generate - -from the root directory or install ack-generate using: - - go get -u github.com/aws/aws-controllers-k8s/cmd/ack-generate" 1>&2 - exit 1; - fi -fi -SERVICE=$(echo "$1" | tr '[:upper:]' '[:lower:]') - -# Source code for the controller will be in a separate repo, typically in -# $GOPATH/src/github.com/aws-controllers-k8s/$AWS_SERVICE-controller/ -DEFAULT_SERVICE_CONTROLLER_SOURCE_PATH="$ROOT_DIR/../$SERVICE-controller" -SERVICE_CONTROLLER_SOURCE_PATH=${SERVICE_CONTROLLER_SOURCE_PATH:-$DEFAULT_SERVICE_CONTROLLER_SOURCE_PATH} - -K8S_RBAC_ROLE_NAME=${K8S_RBAC_ROLE_NAME:-"ack-$SERVICE-controller"} -ACK_GENERATE_SERVICE_ACCOUNT_NAME=${ACK_GENERATE_SERVICE_ACCOUNT_NAME:-"ack-$SERVICE-controller"} - -DEFAULT_IMAGE_REPOSITORY="public.ecr.aws/aws-controllers-k8s/$SERVICE-controller" -ACK_GENERATE_IMAGE_REPOSITORY=${ACK_GENERATE_IMAGE_REPOSITORY:-"$DEFAULT_IMAGE_REPOSITORY"} - -if [[ ! -d $SERVICE_CONTROLLER_SOURCE_PATH ]]; then - echo "Error evaluating SERVICE_CONTROLLER_SOURCE_PATH environment variable:" 1>&2 - echo "$SERVICE_CONTROLLER_SOURCE_PATH is not a directory." 1>&2 - echo "${USAGE}" - exit 1 -fi - -# If the release version is not provided, check if the source controller -# repository has a Git tag on it. If it does, use that as the version. If it -# does not, use "v0.0.0-non-release-version". -# -# This non-release version will allow generation of release artifacts and -# executing presubmit 'release-test' job on those artifacts. -# ACK postsubmit release job makes sure this version does not get released to -# public ecr repository. -# -# Using a static non-release version works because this is only a placeholder -# value which gets replaced during presubmit 'release-test' job. Having a -# default non-release value also helps AWS service teams to develop the -# controller without worrying about the version until actual controller -# release. -pushd "$SERVICE_CONTROLLER_SOURCE_PATH" 1>/dev/null - RELEASE_VERSION=${RELEASE_VERSION:-$(git describe --tags --abbrev=0 2>/dev/null || echo $NON_RELEASE_VERSION)} -popd 1>/dev/null - -if [[ $RELEASE_VERSION != "$NON_RELEASE_VERSION" ]]; then - # validate that release version is in the format vx.y.z , where x,y,z are - # positive real numbers - if ! (echo "$RELEASE_VERSION" | grep -Eq "^v[0-9]+\.[0-9]+\.[0-9]+$"); then - echo "Release version should have following regex format: ^v[0-9]+\.[0-9]+\.[0-9]+$" - exit 1 - fi -fi - -if [ -z "$AWS_SDK_GO_VERSION" ]; then - AWS_SDK_GO_VERSION=$(cat "$SERVICE_CONTROLLER_SOURCE_PATH/apis/$ACK_GENERATE_API_VERSION/ack-generate-metadata.yaml" | yq ".aws_sdk_go_version" -r) -fi - -# If there's a generator.yaml in the service's directory and the caller hasn't -# specified an override, use that. -if [ -z "$ACK_GENERATE_CONFIG_PATH" ]; then - if [ -f "$SERVICE_CONTROLLER_SOURCE_PATH/generator.yaml" ]; then - ACK_GENERATE_CONFIG_PATH="$SERVICE_CONTROLLER_SOURCE_PATH/generator.yaml" - fi -fi - -# If there's a metadata.yaml in the service's directory and the caller hasn't -# specified an override, use that. -if [ -z "$ACK_METADATA_CONFIG_PATH" ]; then - if [ -f "$SERVICE_CONTROLLER_SOURCE_PATH/metadata.yaml" ]; then - ACK_METADATA_CONFIG_PATH="$SERVICE_CONTROLLER_SOURCE_PATH/metadata.yaml" - fi -fi - -# If there's a documentation.yaml in the service's directory and the caller hasn't -# specified an override, use that. -if [ -z "$ACK_DOCUMENTATION_CONFIG_PATH" ]; then - if [ -f "$SERVICE_CONTROLLER_SOURCE_PATH/documentation.yaml" ]; then - ACK_DOCUMENTATION_CONFIG_PATH="$SERVICE_CONTROLLER_SOURCE_PATH/documentation.yaml" - fi -fi - -helm_output_dir="$SERVICE_CONTROLLER_SOURCE_PATH/helm" -ag_args=("$SERVICE" "$RELEASE_VERSION" -o "$SERVICE_CONTROLLER_SOURCE_PATH" --template-dirs "$TEMPLATES_DIR" --aws-sdk-go-version "$AWS_SDK_GO_VERSION") -if [ -n "$ACK_GENERATE_CACHE_DIR" ]; then - ag_args=("${ag_args[@]}" --cache-dir "$ACK_GENERATE_CACHE_DIR") -fi -if [ -n "$ACK_GENERATE_OUTPUT_PATH" ]; then - ag_args=("${ag_args[@]}" --output "$ACK_GENERATE_OUTPUT_PATH") - helm_output_dir="$ACK_GENERATE_OUTPUT_PATH/helm" -fi -if [ -n "$ACK_GENERATE_CONFIG_PATH" ]; then - ag_args=("${ag_args[@]}" --generator-config-path "$ACK_GENERATE_CONFIG_PATH") -fi -if [ -n "$ACK_METADATA_CONFIG_PATH" ]; then - ag_args=("${ag_args[@]}" --metadata-config-path "$ACK_METADATA_CONFIG_PATH") -fi -if [ -n "$ACK_DOCUMENTATION_CONFIG_PATH" ]; then - ag_args=("${ag_args[@]}" --documentation-config-path "$ACK_DOCUMENTATION_CONFIG_PATH") -fi -if [ -n "$ACK_GENERATE_IMAGE_REPOSITORY" ]; then - ag_args=("${ag_args[@]}" --image-repository "$ACK_GENERATE_IMAGE_REPOSITORY") -fi -if [ -n "$ACK_GENERATE_SERVICE_ACCOUNT_NAME" ]; then - ag_args=("${ag_args[@]}" --service-account-name "$ACK_GENERATE_SERVICE_ACCOUNT_NAME") -fi - -echo "Building release artifacts for $SERVICE-$RELEASE_VERSION" -$ACK_GENERATE_BIN_PATH release "${ag_args[@]}" - -pushd "$RUNTIME_DIR/apis/core/$RUNTIME_API_VERSION" 1>/dev/null - -echo "Generating common custom resource definitions" -controller-gen crd:allowDangerousTypes=true paths=./... output:crd:artifacts:config="$helm_output_dir/crds" - -popd 1>/dev/null - -pushd "$SERVICE_CONTROLLER_SOURCE_PATH/apis/$ACK_GENERATE_API_VERSION" 1>/dev/null - -echo "Generating custom resource definitions for $SERVICE" -controller-gen crd:allowDangerousTypes=true paths=./... output:crd:artifacts:config="$helm_output_dir/crds" - -popd 1>/dev/null - -pushd "$SERVICE_CONTROLLER_SOURCE_PATH/pkg/resource" 1>/dev/null - -echo "Generating RBAC manifests for $SERVICE" -controller-gen rbac:roleName="$K8S_RBAC_ROLE_NAME" paths=./... output:rbac:artifacts:config="$helm_output_dir/templates" -# controller-gen rbac outputs a ClusterRole definition in a -# $config_output_dir/rbac/role.yaml file. We additionally add the ability by -# for the user to specify if they want the role to be ClusterRole or Role by specifying installation scope -# in the helm values.yaml. - -# NOTE(a-hilaly): This is some very bad bash-fu, i'm having thoughts about rewriting this hacky code -# in Go or something else. Maybe we need to rework all our generation scripts to be more modular and -# easier to maintain. - -# First we trim the first 6 lines of the role.yaml file (which is the apiVersion, kind, metadata ...) -# this will leave us the rules section of the role.yaml file. We then append the rules section to the -# _helpers-patch.yaml file which is a file that will be included in the _helpers.tpl file. This will -# allow us to use the rules section in the _helpers.tpl file to generate the correct role/clusterrole. -tail -n +6 "$helm_output_dir/templates/role.yaml" > "$helm_output_dir/templates/_helpers-patch.yaml" -helpers_patch_path="$helm_output_dir/templates/_helpers-patch.yaml" - -# Some sed-fu to fill the "controller-role-rules" section. Urgh. -sed '/SEDREPLACERULES/{ - r '$helpers_patch_path' - d -}' $helm_output_dir/templates/_helpers.tpl > $helm_output_dir/templates/_helpers-new.tpl -mv $helm_output_dir/templates/_helpers-new.tpl $helm_output_dir/templates/_helpers.tpl - -rm "$helm_output_dir/templates/role.yaml" -rm "$helpers_patch_path" - -popd 1>/dev/null - -if [[ $ACK_GENERATE_OLM == "true" ]]; then - echo "Generating operator lifecycle manager bundle assets for $SERVICE" - - DEFAULT_ACK_GENERATE_OLMCONFIG_PATH="$SERVICE_CONTROLLER_SOURCE_PATH/olm/olmconfig.yaml" - ACK_GENERATE_OLMCONFIG_PATH=${ACK_GENERATE_OLMCONFIG_PATH:-$DEFAULT_ACK_GENERATE_OLMCONFIG_PATH} - - ag_olm_args=("$SERVICE" "$RELEASE_VERSION" -o "$SERVICE_CONTROLLER_SOURCE_PATH" --template-dirs "$TEMPLATES_DIR" --olm-config "$ACK_GENERATE_OLMCONFIG_PATH" --aws-sdk-go-version "$AWS_SDK_GO_VERSION") - - if [ -n "$ACK_GENERATE_CONFIG_PATH" ]; then - ag_olm_args=("${ag_olm_args[@]}" --generator-config-path "$ACK_GENERATE_CONFIG_PATH") - fi - if [ -n "$ACK_GENERATE_IMAGE_REPOSITORY" ]; then - ag_olm_args=("${ag_olm_args[@]}" --image-repository "$ACK_GENERATE_IMAGE_REPOSITORY") - fi - if [ -n "$ACK_METADATA_CONFIG_PATH" ]; then - ag_olm_args=("${ag_olm_args[@]}" --metadata-config-path "$ACK_METADATA_CONFIG_PATH") - fi - if [ -n "$ACK_DOCUMENTATION_CONFIG_PATH" ]; then - ag_olm_args=("${ag_olm_args[@]}" --documentation-config-path "$ACK_DOCUMENTATION_CONFIG_PATH") - fi - if [ -n "$ACK_GENERATE_CACHE_DIR" ]; then - ag_olm_args=("${ag_olm_args[@]}" --cache-dir "$ACK_GENERATE_CACHE_DIR") - fi - - $ACK_GENERATE_BIN_PATH olm "${ag_olm_args[@]}" - "$SCRIPTS_DIR"/olm-create-bundle.sh "$SERVICE" "$RELEASE_VERSION" -fi diff --git a/scripts/build-controller.sh b/scripts/build-controller.sh deleted file mode 100755 index abdb7ee7..00000000 --- a/scripts/build-controller.sh +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env bash - -# A script that builds a single ACK service controller for an AWS service API - -set -eo pipefail - -SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -ROOT_DIR="$SCRIPTS_DIR/.." - -source "$SCRIPTS_DIR/lib/common.sh" - -check_is_installed controller-gen "You can install controller-gen with the helper scripts/install-controller-gen.sh" - -if ! k8s_controller_gen_version_equals "$CONTROLLER_TOOLS_VERSION"; then - echo "FATAL: Existing version of controller-gen \"$(controller-gen --version)\", required version is $CONTROLLER_TOOLS_VERSION." - echo "FATAL: Please uninstall controller-gen and install the required version with scripts/install-controller-gen.sh." - exit 1 -fi - -ACK_GENERATE_CACHE_DIR=${ACK_GENERATE_CACHE_DIR:-"$HOME/.cache/aws-controllers-k8s"} -# The ack-generate code generator is in a separate source code repository, -# typically at $GOPATH/src/github.com/aws-controllers-k8s/code-generator -DEFAULT_ACK_GENERATE_BIN_PATH="$ROOT_DIR/bin/ack-generate" -ACK_GENERATE_BIN_PATH=${ACK_GENERATE_BIN_PATH:-$DEFAULT_ACK_GENERATE_BIN_PATH} -ACK_GENERATE_API_VERSION=${ACK_GENERATE_API_VERSION:-"v1alpha1"} -ACK_GENERATE_CONFIG_PATH=${ACK_GENERATE_CONFIG_PATH:-""} -ACK_METADATA_CONFIG_PATH=${ACK_METADATA_CONFIG_PATH:-""} -ACK_DOCUMENTATION_CONFIG_PATH=${ACK_DOCUMENTATION_CONFIG_PATH:-""} -ACK_GENERATE_SERVICE_ACCOUNT_NAME=${ACK_GENERATE_SERVICE_ACCOUNT_NAME:-"ack-$SERVICE-controller"} -AWS_SDK_GO_VERSION=${AWS_SDK_GO_VERSION:-""} -DEFAULT_RUNTIME_CRD_DIR="$ROOT_DIR/../../aws-controllers-k8s/runtime/config" -RUNTIME_CRD_DIR=${RUNTIME_CRD_DIR:-$DEFAULT_RUNTIME_CRD_DIR} -K8S_RBAC_ROLE_NAME=${K8S_RBAC_ROLE_NAME:-"ack-$SERVICE-controller"} - -USAGE=" -Usage: - $(basename "$0") - - should be an AWS service API aliases that you wish to build -- e.g. -'s3' 'sns' or 'sqs' - -Environment variables: - ACK_GENERATE_CACHE_DIR: Overrides the directory used for caching AWS API - models used by the ack-generate tool. - Default: $ACK_GENERATE_CACHE_DIR - ACK_GENERATE_BIN_PATH: Overrides the path to the the ack-generate binary. - Default: $ACK_GENERATE_BIN_PATH - ACK_GENERATE_API_VERSION: Overrides the version of the Kubernetes API objects - generated by the ack-generate apis command. If not - specified, and the service controller has been - previously generated, the latest generated API - version is used. If the service controller has yet - to be generated, 'v1alpha1' is used. - ACK_GENERATE_CONFIG_PATH: Specify a path to the generator config YAML file to - instruct the code generator for the service. - Default: generator.yaml - ACK_METADATA_CONFIG_PATH: Specify a path to the metadata config YAML file to - instruct the code generator for the service. - Default: metadata.yaml - ACK_DOCUMENTATION_CONFIG_PATH: Specify a path to the documentation config YAML file to - instruct the code generator for the service. - Default: documentation.yaml - ACK_GENERATE_SERVICE_ACCOUNT_NAME: Name of the Kubernetes Service Account and - Cluster Role to use in Helm chart. - Default: $ACK_GENERATE_SERVICE_ACCOUNT_NAME - AWS_SDK_GO_VERSION: Overrides the version of github.com/aws/aws-sdk-go used - by 'ack-generate' to fetch the service API Specifications. - Default: Version of aws/aws-sdk-go in service go.mod - TEMPLATE_DIRS: Overrides the list of directories containing ack-generate - templates. - Default: $TEMPLATE_DIRS - K8S_RBAC_ROLE_NAME: Name of the Kubernetes Role to use when generating - the RBAC manifests for the custom resource - definitions. - Default: $K8S_RBAC_ROLE_NAME -" - -if [ $# -ne 1 ]; then - echo "ERROR: $(basename "$0") only accepts a single parameter" 1>&2 - echo "$USAGE" - exit 1 -fi - -if [ ! -f "$ACK_GENERATE_BIN_PATH" ]; then - if is_installed "ack-generate"; then - ACK_GENERATE_BIN_PATH=$(which "ack-generate") - else - echo "ERROR: Unable to find an ack-generate binary. -Either set the ACK_GENERATE_BIN_PATH to a valid location or -run: - - make build-ack-generate - -from the root directory or install ack-generate using: - - go get -u -tags codegen github.com/aws-controllers-k8s/code-generator/cmd/ack-generate" 1>&2 - exit 1; - fi -fi -SERVICE=$(echo "$1" | tr '[:upper:]' '[:lower:]') - -# Source code for the controller will be in a separate repo, typically in -# $GOPATH/src/github.com/aws-controllers-k8s/$AWS_SERVICE-controller/ -DEFAULT_SERVICE_CONTROLLER_SOURCE_PATH="$ROOT_DIR/../$SERVICE-controller" -SERVICE_CONTROLLER_SOURCE_PATH=${SERVICE_CONTROLLER_SOURCE_PATH:-$DEFAULT_SERVICE_CONTROLLER_SOURCE_PATH} - -if [[ ! -d $SERVICE_CONTROLLER_SOURCE_PATH ]]; then - echo "Error evaluating SERVICE_CONTROLLER_SOURCE_PATH environment variable:" 1>&2 - echo "$SERVICE_CONTROLLER_SOURCE_PATH is not a directory." 1>&2 - echo "${USAGE}" - exit 1 -fi - -BOILERPLATE_TXT_PATH="$ROOT_DIR/templates/boilerplate.txt" -DEFAULT_TEMPLATE_DIRS="$ROOT_DIR/templates" -# If the service controller source repository has a templates/ directory, add -# that as a template base directory to search for templates in. -# Note that ack-generate accepts multiple template paths for its -# `--template-dirs` CLI flag. The order of these template directories is -# important, as it indicates the order in which the code generator will search -# for template files to use. Developers of a service controller can essentially -# "override" the default template used for various things by adding a -# same-named template file into a templates/ directory in their service -# controller. -if [[ -d "$SERVICE_CONTROLLER_SOURCE_PATH/templates" ]]; then - DEFAULT_TEMPLATE_DIRS="$SERVICE_CONTROLLER_SOURCE_PATH/templates,$DEFAULT_TEMPLATE_DIRS" - if [[ -f "$SERVICE_CONTROLLER_SOURCE_PATH/templates/boilerplate.txt" ]]; then - BOILERPLATE_TXT_PATH="$SERVICE_CONTROLLER_SOURCE_PATH/templates/boilerplate.txt" - fi -fi - -TEMPLATE_DIRS=${TEMPLATE_DIRS:-$DEFAULT_TEMPLATE_DIRS} - -config_output_dir="$SERVICE_CONTROLLER_SOURCE_PATH/config/" - -echo "Copying common custom resource definitions into $SERVICE" -mkdir -p "$config_output_dir/crd/common" -cp -r "$RUNTIME_CRD_DIR"/crd/* "$config_output_dir/crd/common/" - -if [ -z "$AWS_SDK_GO_VERSION" ]; then - AWS_SDK_GO_VERSION=$(cat "$SERVICE_CONTROLLER_SOURCE_PATH/apis/$ACK_GENERATE_API_VERSION/ack-generate-metadata.yaml" | yq ".aws_sdk_go_version" -r) -fi - -# If there's a generator.yaml in the service's directory and the caller hasn't -# specified an override, use that. -if [ -z "$ACK_GENERATE_CONFIG_PATH" ]; then - if [ -f "$SERVICE_CONTROLLER_SOURCE_PATH/generator.yaml" ]; then - ACK_GENERATE_CONFIG_PATH="$SERVICE_CONTROLLER_SOURCE_PATH/generator.yaml" - fi -fi - -# If there's a metadata.yaml in the service's directory and the caller hasn't -# specified an override, use that. -if [ -z "$ACK_METADATA_CONFIG_PATH" ]; then - if [ -f "$SERVICE_CONTROLLER_SOURCE_PATH/metadata.yaml" ]; then - ACK_METADATA_CONFIG_PATH="$SERVICE_CONTROLLER_SOURCE_PATH/metadata.yaml" - fi -fi - -# If there's a documentation.yaml in the service's directory and the caller hasn't -# specified an override, use that. -if [ -z "$ACK_DOCUMENTATION_CONFIG_PATH" ]; then - if [ -f "$SERVICE_CONTROLLER_SOURCE_PATH/documentation.yaml" ]; then - ACK_DOCUMENTATION_CONFIG_PATH="$SERVICE_CONTROLLER_SOURCE_PATH/documentation.yaml" - fi -fi - -ag_args=("$SERVICE" -o "$SERVICE_CONTROLLER_SOURCE_PATH" --template-dirs "$TEMPLATE_DIRS") -if [ -n "$ACK_GENERATE_CACHE_DIR" ]; then - ag_args=("${ag_args[@]}" --cache-dir "$ACK_GENERATE_CACHE_DIR") -fi - -apis_args=(apis "${ag_args[@]}") -if [ -n "$ACK_GENERATE_API_VERSION" ]; then - apis_args=("${apis_args[@]}" --version "$ACK_GENERATE_API_VERSION") -fi - -if [ -n "$ACK_GENERATE_CONFIG_PATH" ]; then - ag_args=("${ag_args[@]}" --generator-config-path "$ACK_GENERATE_CONFIG_PATH") - apis_args=("${apis_args[@]}" --generator-config-path "$ACK_GENERATE_CONFIG_PATH") -fi - -if [ -n "$ACK_METADATA_CONFIG_PATH" ]; then - ag_args=("${ag_args[@]}" --metadata-config-path "$ACK_METADATA_CONFIG_PATH") - apis_args=("${apis_args[@]}" --metadata-config-path "$ACK_METADATA_CONFIG_PATH") -fi - -if [ -n "$ACK_DOCUMENTATION_CONFIG_PATH" ]; then - ag_args=("${ag_args[@]}" --documentation-config-path "$ACK_DOCUMENTATION_CONFIG_PATH") - apis_args=("${apis_args[@]}" --documentation-config-path "$ACK_DOCUMENTATION_CONFIG_PATH") -fi - -if [ -n "$AWS_SDK_GO_VERSION" ]; then - ag_args=("${ag_args[@]}" --aws-sdk-go-version "$AWS_SDK_GO_VERSION") - apis_args=("${apis_args[@]}" --aws-sdk-go-version "$AWS_SDK_GO_VERSION") -fi - -if [ -n "$ACK_GENERATE_SERVICE_ACCOUNT_NAME" ]; then - ag_args=("${ag_args[@]}" --service-account-name "$ACK_GENERATE_SERVICE_ACCOUNT_NAME") -fi - -echo "Building Kubernetes API objects for $SERVICE" -if ! $ACK_GENERATE_BIN_PATH "${apis_args[@]}"; then - exit 2 -fi - -pushd "$SERVICE_CONTROLLER_SOURCE_PATH/apis/$ACK_GENERATE_API_VERSION" 1>/dev/null - -echo "Generating deepcopy code for $SERVICE" -controller-gen object:headerFile="$BOILERPLATE_TXT_PATH" paths=./... - -echo "Generating custom resource definitions for $SERVICE" -# Latest version of controller-gen (master) is required for following two reasons -# a) support for pointer values in map https://github.com/kubernetes-sigs/controller-tools/pull/317 -# b) support for float type (allowDangerousTypes) https://github.com/kubernetes-sigs/controller-tools/pull/449 -controller-gen crd:allowDangerousTypes=true paths=./... output:crd:artifacts:config="$config_output_dir/crd/bases" - -popd 1>/dev/null - -echo "Building service controller for $SERVICE" -controller_args=(controller "${ag_args[@]}") -if ! $ACK_GENERATE_BIN_PATH "${controller_args[@]}"; then - exit 2 -fi - -pushd "$SERVICE_CONTROLLER_SOURCE_PATH/pkg/resource" 1>/dev/null -echo "Running GO mod tidy" -go mod tidy 1>/dev/null -echo "Generating RBAC manifests for $SERVICE" -controller-gen rbac:roleName="$K8S_RBAC_ROLE_NAME" paths=./... output:rbac:artifacts:config="$config_output_dir/rbac" -# controller-gen rbac outputs a ClusterRole definition in a -# $config_output_dir/rbac/role.yaml file. We have some other standard Role -# files for a reader and writer role, so here we rename the `role.yaml` file to -# `cluster-role-controller.yaml` to better reflect what is in that file. -mv "$config_output_dir/rbac/role.yaml" "$config_output_dir/rbac/cluster-role-controller.yaml" -# Copy definitions for json patches which allow the user to patch the controller -# with Role/Rolebinding and be purely namespaced scoped instead of using Cluster/ClusterRoleBinding -# using kustomize -mkdir -p "$config_output_dir/overlays/namespaced" -cp -r "$ROOT_DIR"/templates/config/overlays/namespaced/*.json "$config_output_dir/overlays/namespaced" - -popd 1>/dev/null - -echo "Running gofmt against generated code for $SERVICE" -gofmt -w "$SERVICE_CONTROLLER_SOURCE_PATH" -go install golang.org/x/tools/cmd/goimports@latest -goimports -w "$SERVICE_CONTROLLER_SOURCE_PATH" - -echo "Updating additional GitHub repository maintenance files" -cp "$ROOT_DIR"/CODE_OF_CONDUCT.md "$SERVICE_CONTROLLER_SOURCE_PATH"/CODE_OF_CONDUCT.md -cp "$ROOT_DIR"/CONTRIBUTING.md "$SERVICE_CONTROLLER_SOURCE_PATH"/CONTRIBUTING.md -cp "$ROOT_DIR"/GOVERNANCE.md "$SERVICE_CONTROLLER_SOURCE_PATH"/GOVERNANCE.md -cp "$ROOT_DIR"/LICENSE "$SERVICE_CONTROLLER_SOURCE_PATH"/LICENSE -cp "$ROOT_DIR"/NOTICE "$SERVICE_CONTROLLER_SOURCE_PATH"/NOTICE diff --git a/templates/Makefile.tpl b/templates/Makefile.tpl new file mode 100644 index 00000000..6d6bc427 --- /dev/null +++ b/templates/Makefile.tpl @@ -0,0 +1,66 @@ +# Code generated by ack-generate. DO NOT EDIT. +# +# Makefile for the ACK {{ .ServicePackageName }} controller. +# Run 'make help' to see available targets. + +SHELL := /bin/bash + +SERVICE := {{ .ServicePackageName }} + +# Build configuration +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "v0.0.0") +GITCOMMIT = $(shell git rev-parse HEAD) +BUILDDATE = $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') +GO_LDFLAGS = -ldflags "-X main.version=$(VERSION) -X main.buildHash=$(GITCOMMIT) -X main.buildDate=$(BUILDDATE)" + +# Controller runtime configuration +AWS_REGION ?= us-west-2 + +.PHONY: all build test local-test ensure-ack-generate generate release check-crd-compatibility run version help + +all: test + +build: ## Build the controller binary + @go build $(GO_LDFLAGS) -o bin/controller cmd/controller/main.go + +test: ## Run unit tests + @go test ./... + +local-test: ## Run unit tests using go.local.mod + @go test -modfile=go.local.mod ./... + +ACK_GENERATE_VERSION ?= main + +ensure-ack-generate: + @go install github.com/aws-controllers-k8s/code-generator/cmd/ack-generate@$(ACK_GENERATE_VERSION) + +generate: ensure-ack-generate release ## Regenerate controller code and release artifacts + @ack-generate controller $(SERVICE) + +RELEASE_VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0-non-release-version") + +release: ensure-ack-generate ## Generate release artifacts (override with RELEASE_VERSION=v1.2.4) + @ack-generate release $(SERVICE) $(RELEASE_VERSION) + +BASE_REF ?= main +CRD_PATHS ?= config/crd/bases,helm/crds + +check-crd-compatibility: ensure-ack-generate ## Check CRDs for breaking changes against BASE_REF + @ack-generate crd-compat-check --base-ref=$(BASE_REF) --crd-paths=$(CRD_PATHS) + +run: ## Run the controller locally (AWS_REGION=us-west-2) + @go run ./cmd/controller/main.go \ + --aws-region=$(AWS_REGION) \ + --enable-development-logging \ + --log-level=debug + +version: ## Print the current version + @echo $(VERSION) + +help: ## Show this help + @echo "Usage: make [target]" + @echo "" + @echo "Targets:" + @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*##"}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' + +-include Makefile.custom