diff --git a/.golangci.yml b/.golangci.yml index f98fee061e9b..88d58d410ac1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,12 +1,6 @@ run: timeout: 5m - skip-files: - - "zz_generated.*\\.go$" - skip-dirs: - - ".*/mocks" - - "manager/tilt_modules" - - "internal/aws-sdk-go-v2" - - "pkg/providers/snow/api/v1beta1" + linters: enable: - gofumpt @@ -15,6 +9,7 @@ linters: - nakedret - gocyclo - revive + linters-settings: gci: sections: @@ -34,9 +29,20 @@ linters-settings: gocyclo: # Minimal code complexity to report. min-complexity: 10 + issues: max-same-issues: 0 max-issues-per-linter: 0 + + exclude-files: + - "zz_generated.*\\.go$" + + exclude-dirs: + - ".*/mocks" + - "manager/tilt_modules" + - "internal/aws-sdk-go-v2" + - "pkg/providers/snow/api/v1beta1" + include: - EXC0012 # EXC0012 revive: exported (.+) should have comment( \(or a comment on this block\))? or be unexported - EXC0014 # EXC0014 revive: comment on exported (.+) should be of the form "(.+)..." diff --git a/release/api/v1alpha1/artifact_types.go b/release/api/v1alpha1/artifact_types.go index 625f07998f2e..19291d330bc7 100644 --- a/release/api/v1alpha1/artifact_types.go +++ b/release/api/v1alpha1/artifact_types.go @@ -16,6 +16,8 @@ package v1alpha1 import "strings" +// Image represents a container image asset along with metadata such as OS, +// architecture, and registry information. type Image struct { // +kubebuilder:validation:Required // The asset name @@ -42,10 +44,14 @@ type Image struct { ImageDigest string `json:"imageDigest,omitempty"` } +// VersionedImage returns the full URI of the Image, including registry, +// repository, and tag or digest. func (i Image) VersionedImage() string { return i.URI } +// Image returns the repository URI of the Image, excluding the tag or digest +// if one is present. func (i Image) Image() string { lastInd := strings.LastIndex(i.URI, ":") if lastInd == -1 { @@ -54,6 +60,7 @@ func (i Image) Image() string { return i.URI[:lastInd] } +// Tag returns the tag portion of the Image's URI if present, otherwise an empty string. func (i Image) Tag() string { lastInd := strings.LastIndex(i.URI, ":") if lastInd == -1 || lastInd == len(i.URI)-1 { @@ -62,6 +69,8 @@ func (i Image) Tag() string { return i.URI[lastInd+1:] } +// ChartName constructs a typical Helm chart artifact name (with ".tgz") +// from the Image's name by replacing the last colon with a hyphen. func (i Image) ChartName() string { lastInd := strings.LastIndex(i.Image(), "/") if lastInd == -1 { @@ -73,6 +82,7 @@ func (i Image) ChartName() string { return chart } +// Registry returns the registry portion of the Image URI (the substring before the first slash). func (i *Image) Registry() string { result := strings.Split(i.URI, "/") if len(result) < 1 { @@ -81,6 +91,7 @@ func (i *Image) Registry() string { return result[0] } +// Repository returns the repository name (between the registry and the tag/digest). func (i *Image) Repository() string { rol := strings.TrimPrefix(i.URI, i.Registry()+"/") result := strings.Split(rol, "@") @@ -94,6 +105,7 @@ func (i *Image) Repository() string { return result[0] } +// Digest returns the SHA digest portion (after '@') of the Image URI, if present. func (i *Image) Digest() string { rol := strings.TrimPrefix(i.URI, i.Registry()+"/") result := strings.Split(rol, "@") @@ -103,6 +115,7 @@ func (i *Image) Digest() string { return result[1] } +// Version returns the tag portion (after ':') of the Image URI, if present, or empty if the URI uses digests. func (i *Image) Version() string { rol := strings.TrimPrefix(i.URI, i.Registry()+"/") result := strings.Split(rol, "@") @@ -116,6 +129,8 @@ func (i *Image) Version() string { return "" } +// Archive represents an archive asset (e.g. tarball) along with its OS/architecture metadata, +// and checksums for file integrity. type Archive struct { // +kubebuilder:validation:Required // The asset name @@ -138,14 +153,18 @@ type Archive struct { // +kubebuilder:validation:Required // The URI where the asset is located URI string `json:"uri,omitempty"` + // +kubebuilder:validation:Required // The sha512 of the asset, only applies for 'file' store SHA512 string `json:"sha512,omitempty"` + // +kubebuilder:validation:Required // The sha256 of the asset, only applies for 'file' store SHA256 string `json:"sha256,omitempty"` } +// Manifest represents a reference to a manifest, typically containing +// further resource definitions or configurations. type Manifest struct { // +kubebuilder:validation:Required // URI points to the manifest yaml file diff --git a/release/api/v1alpha1/artifacts.go b/release/api/v1alpha1/artifacts.go index 40c8c58fbc1e..8065596076bc 100644 --- a/release/api/v1alpha1/artifacts.go +++ b/release/api/v1alpha1/artifacts.go @@ -14,6 +14,7 @@ package v1alpha1 +// Manifests returns a map of manifests for different components in a VersionsBundle. func (vb *VersionsBundle) Manifests() map[string][]*string { return map[string][]*string{ "core-cluster-api": { @@ -83,12 +84,14 @@ func (vb *VersionsBundle) Manifests() map[string][]*string { } } +// Ovas returns a list of OVA archives in a VersionsBundle. func (vb *VersionsBundle) Ovas() []Archive { return []Archive{ vb.EksD.Ova.Bottlerocket, } } +// CloudStackImages returns images needed for the CloudStack provider in a VersionsBundle. func (vb *VersionsBundle) CloudStackImages() []Image { return []Image{ vb.CloudStack.ClusterAPIController, @@ -97,6 +100,7 @@ func (vb *VersionsBundle) CloudStackImages() []Image { } } +// VsphereImages returns images needed for the vSphere provider in a VersionsBundle. func (vb *VersionsBundle) VsphereImages() []Image { return []Image{ vb.VSphere.ClusterAPIController, @@ -106,6 +110,7 @@ func (vb *VersionsBundle) VsphereImages() []Image { } } +// DockerImages returns images needed for the Docker provider in a VersionsBundle. func (vb *VersionsBundle) DockerImages() []Image { return []Image{ vb.Docker.KubeProxy, @@ -113,6 +118,7 @@ func (vb *VersionsBundle) DockerImages() []Image { } } +// SnowImages returns images needed for the Snow provider in a VersionsBundle. func (vb *VersionsBundle) SnowImages() []Image { i := make([]Image, 0, 2) if vb.Snow.KubeVip.URI != "" { @@ -128,6 +134,7 @@ func (vb *VersionsBundle) SnowImages() []Image { return i } +// TinkerbellImages returns images needed for the Tinkerbell provider in a VersionsBundle. func (vb *VersionsBundle) TinkerbellImages() []Image { return []Image{ vb.Tinkerbell.ClusterAPIController, @@ -154,6 +161,7 @@ func (vb *VersionsBundle) TinkerbellImages() []Image { } } +// NutanixImages returns images needed for the Nutanix provider in a VersionsBundle. func (vb *VersionsBundle) NutanixImages() []Image { i := make([]Image, 0, 2) if vb.Nutanix.ClusterAPIController.URI != "" { @@ -167,6 +175,7 @@ func (vb *VersionsBundle) NutanixImages() []Image { return i } +// SharedImages returns images that are shared across different providers in a VersionsBundle. func (vb *VersionsBundle) SharedImages() []Image { return []Image{ vb.Bootstrap.Controller, @@ -204,6 +213,7 @@ func (vb *VersionsBundle) SharedImages() []Image { } } +// Images returns all images from the VersionsBundle by aggregating those from different providers. func (vb *VersionsBundle) Images() []Image { groupedImages := [][]Image{ vb.SharedImages(), @@ -228,6 +238,7 @@ func (vb *VersionsBundle) Images() []Image { return images } +// Charts returns a map of Helm chart images used by different components in a VersionsBundle. func (vb *VersionsBundle) Charts() map[string]*Image { return map[string]*Image{ "cilium": &vb.Cilium.HelmChart, diff --git a/release/api/v1alpha1/artifacts_test.go b/release/api/v1alpha1/artifacts_test.go index 66078706b21d..5fb7e2afa23b 100644 --- a/release/api/v1alpha1/artifacts_test.go +++ b/release/api/v1alpha1/artifacts_test.go @@ -14,6 +14,7 @@ package v1alpha1_test +//nolint:revive import ( "testing" diff --git a/release/api/v1alpha1/bundle_types.go b/release/api/v1alpha1/bundle_types.go index af2c5a2bac1a..90cb96f028de 100644 --- a/release/api/v1alpha1/bundle_types.go +++ b/release/api/v1alpha1/bundle_types.go @@ -42,6 +42,7 @@ type Bundles struct { Status BundlesStatus `json:"status,omitempty"` } +// DefaultEksAToolsImage returns the default EKS Anywhere Tools image. func (b *Bundles) DefaultEksAToolsImage() Image { return b.Spec.VersionsBundles[0].Eksa.CliTools } @@ -59,6 +60,7 @@ func init() { SchemeBuilder.Register(&Bundles{}, &BundlesList{}) } +// VersionsBundle defines the versions of each component for a specific Kubernetes version in the Bundles CRD. type VersionsBundle struct { KubeVersion string `json:"kubeVersion"` EndOfStandardSupport string `json:"endOfStandardSupport,omitempty"` @@ -87,6 +89,7 @@ type VersionsBundle struct { Aws *AwsBundle `json:"aws,omitempty"` } +// EksDRelease defines the EKS-D specific release information. type EksDRelease struct { // +kubebuilder:validation:Required Name string `json:"name,omitempty"` @@ -102,7 +105,7 @@ type EksDRelease struct { // +kubebuilder:validation:Required // Url pointing to the EKS-D release manifest using which // assets where created - EksDReleaseUrl string `json:"manifestUrl,omitempty"` + EksDReleaseUrl string `json:"manifestUrl,omitempty"` //nolint:revive // +kubebuilder:validation:Required // Git commit the component is built from, before any patches @@ -136,21 +139,24 @@ type EksDRelease struct { Containerd Archive `json:"containerd,omitempty"` } -// UpgraderBundle is a In-place Kubernetes version upgrader bundle. +// UpgraderBundle is a bundle for in-place Kubernetes version upgrader images. type UpgraderBundle struct { Upgrader Image `json:"upgrader"` } +// OSImageBundle defines a set of OS images (e.g., Bottlerocket) for this bundle. type OSImageBundle struct { Bottlerocket Archive `json:"bottlerocket,omitempty"` } +// BottlerocketHostContainersBundle defines the Bottlerocket host containers used by the bundle. type BottlerocketHostContainersBundle struct { Admin Image `json:"admin"` Control Image `json:"control"` KubeadmBootstrap Image `json:"kubeadmBootstrap"` } +// CertManagerBundle defines the Cert Manager version and images for this bundle. type CertManagerBundle struct { Version string `json:"version,omitempty"` Acmesolver Image `json:"acmesolver"` @@ -163,6 +169,7 @@ type CertManagerBundle struct { Manifest Manifest `json:"manifest"` } +// CoreClusterAPI defines the Core Cluster API version and images used in this bundle. type CoreClusterAPI struct { Version string `json:"version"` Controller Image `json:"controller"` @@ -171,6 +178,7 @@ type CoreClusterAPI struct { Metadata Manifest `json:"metadata"` } +// KubeadmBootstrapBundle defines the bootstrap controller images and version for this bundle. type KubeadmBootstrapBundle struct { Version string `json:"version"` Controller Image `json:"controller"` @@ -179,6 +187,7 @@ type KubeadmBootstrapBundle struct { Metadata Manifest `json:"metadata"` } +// KubeadmControlPlaneBundle defines the control plane controller images and version for this bundle. type KubeadmControlPlaneBundle struct { Version string `json:"version"` Controller Image `json:"controller"` @@ -187,6 +196,7 @@ type KubeadmControlPlaneBundle struct { Metadata Manifest `json:"metadata"` } +// AwsBundle defines the AWS-related images and configurations for this bundle. type AwsBundle struct { Version string `json:"version"` Controller Image `json:"controller"` @@ -196,6 +206,7 @@ type AwsBundle struct { Metadata Manifest `json:"metadata"` } +// VSphereBundle defines the vSphere provider images and version for this bundle. type VSphereBundle struct { Version string `json:"version"` ClusterAPIController Image `json:"clusterAPIController"` @@ -211,6 +222,7 @@ type VSphereBundle struct { Syncer *Image `json:"syncer,omitempty"` } +// DockerBundle defines the Docker provider images and version for this bundle. type DockerBundle struct { Version string `json:"version"` Manager Image `json:"manager"` @@ -220,6 +232,7 @@ type DockerBundle struct { Metadata Manifest `json:"metadata"` } +// CloudStackBundle defines the CloudStack provider images and version for this bundle. type CloudStackBundle struct { Version string `json:"version"` ClusterAPIController Image `json:"clusterAPIController"` @@ -229,6 +242,7 @@ type CloudStackBundle struct { Metadata Manifest `json:"metadata"` } +// CiliumBundle defines the Cilium version and images used for CNI in this bundle. type CiliumBundle struct { Version string `json:"version,omitempty"` Cilium Image `json:"cilium"` @@ -237,11 +251,13 @@ type CiliumBundle struct { HelmChart Image `json:"helmChart,omitempty"` } +// KindnetdBundle defines the Kindnetd version and manifest for this bundle. type KindnetdBundle struct { Version string `json:"version,omitempty"` Manifest Manifest `json:"manifest"` } +// FluxBundle defines the Flux components and versions used in this bundle. type FluxBundle struct { Version string `json:"version,omitempty"` SourceController Image `json:"sourceController"` @@ -250,6 +266,7 @@ type FluxBundle struct { NotificationController Image `json:"notificationController"` } +// PackageBundle defines the EKS Anywhere package controller and related images for this bundle. type PackageBundle struct { Version string `json:"version,omitempty"` Controller Image `json:"packageController"` @@ -258,6 +275,7 @@ type PackageBundle struct { HelmChart Image `json:"helmChart,omitempty"` } +// EksaBundle defines EKS Anywhere-specific images and configurations for this bundle. type EksaBundle struct { Version string `json:"version,omitempty"` CliTools Image `json:"cliTools"` @@ -266,6 +284,7 @@ type EksaBundle struct { Components Manifest `json:"components"` } +// EtcdadmBootstrapBundle defines the bootstrap mechanism for external etcd in this bundle. type EtcdadmBootstrapBundle struct { Version string `json:"version"` Controller Image `json:"controller"` @@ -274,6 +293,7 @@ type EtcdadmBootstrapBundle struct { Metadata Manifest `json:"metadata"` } +// EtcdadmControllerBundle defines the external etcd controller images and version for this bundle. type EtcdadmControllerBundle struct { Version string `json:"version"` Controller Image `json:"controller"` @@ -282,6 +302,7 @@ type EtcdadmControllerBundle struct { Metadata Manifest `json:"metadata"` } +// TinkerbellStackBundle defines the Tinkerbell stack components for provisioning bare metal. type TinkerbellStackBundle struct { Actions ActionsBundle `json:"actions"` Boots Image `json:"boots"` @@ -294,7 +315,7 @@ type TinkerbellStackBundle struct { Stack Image `json:"stack"` } -// Tinkerbell Template Actions. +// ActionsBundle defines the Tinkerbell template actions. type ActionsBundle struct { Cexec Image `json:"cexec"` Kexec Image `json:"kexec"` @@ -304,6 +325,7 @@ type ActionsBundle struct { Reboot Image `json:"reboot"` } +// TinkBundle defines the images required for Tinkerbell controller and worker processes. type TinkBundle struct { TinkRelay Image `json:"tinkRelay"` TinkRelayInit Image `json:"tinkRelayInit"` @@ -313,7 +335,7 @@ type TinkBundle struct { Nginx Image `json:"nginx"` } -// Tinkerbell hook OS. +// HookBundle defines the Tinkerbell hook OS images used for provisioning. type HookBundle struct { Bootkit Image `json:"bootkit"` Docker Image `json:"docker"` @@ -322,11 +344,13 @@ type HookBundle struct { Vmlinuz HookArch `json:"vmlinuz"` } +// HookArch defines the Tinkerbell hook architecture-specific artifacts. type HookArch struct { Arm Archive `json:"arm"` Amd Archive `json:"amd"` } +// TinkerbellBundle defines Tinkerbell provider images and configurations for this bundle. type TinkerbellBundle struct { Version string `json:"version"` ClusterAPIController Image `json:"clusterAPIController"` @@ -338,10 +362,12 @@ type TinkerbellBundle struct { TinkerbellStack TinkerbellStackBundle `json:"tinkerbellStack,omitempty"` } +// HaproxyBundle defines the HAProxy image used for this bundle. type HaproxyBundle struct { Image Image `json:"image"` } +// SnowBundle defines the Snow provider images and configurations for this bundle. type SnowBundle struct { Version string `json:"version"` Manager Image `json:"manager"` @@ -351,6 +377,7 @@ type SnowBundle struct { BottlerocketBootstrapSnow Image `json:"bottlerocketBootstrapSnow"` } +// NutanixBundle defines the Nutanix provider images and configurations for this bundle. type NutanixBundle struct { ClusterAPIController Image `json:"clusterAPIController"` CloudProvider Image `json:"cloudProvider,omitempty"` diff --git a/release/api/v1alpha1/bundle_types_test.go b/release/api/v1alpha1/bundle_types_test.go index f1000506dc0d..4bb0ef3aca6b 100644 --- a/release/api/v1alpha1/bundle_types_test.go +++ b/release/api/v1alpha1/bundle_types_test.go @@ -14,6 +14,7 @@ package v1alpha1_test +//nolint:revive import ( "testing" diff --git a/release/api/v1alpha1/eksarelease.go b/release/api/v1alpha1/eksarelease.go index e6d25fb2fba0..b2c1cb04e955 100644 --- a/release/api/v1alpha1/eksarelease.go +++ b/release/api/v1alpha1/eksarelease.go @@ -8,7 +8,7 @@ import ( // EKSAReleaseKind is the Kind of EKSARelease. const EKSAReleaseKind = "EKSARelease" -// Generates the naming convention of EKSARelease from a version. +// GenerateEKSAReleaseName generates the naming convention of EKSARelease from a version. func GenerateEKSAReleaseName(version string) string { version = strings.ReplaceAll(version, "+", "-plus-") return fmt.Sprintf("eksa-%s", strings.ReplaceAll(version, ".", "-")) diff --git a/release/api/v1alpha1/release_types.go b/release/api/v1alpha1/release_types.go index a905b0c732b1..7ad12f9be5d2 100644 --- a/release/api/v1alpha1/release_types.go +++ b/release/api/v1alpha1/release_types.go @@ -82,7 +82,7 @@ type EksARelease struct { // +kubebuilder:validation:Required // Manifest url to parse bundle information from for this EKS-A release - BundleManifestUrl string `json:"bundleManifestUrl"` + BundleManifestUrl string `json:"bundleManifestUrl"` //nolint:revive // +kubebuilder:validation:Required // EKS Anywhere binary bundle @@ -93,6 +93,7 @@ type EksARelease struct { EksACLI PlatformBundle `json:"eksACLI"` } +// BinaryBundle defines the EKS Anywhere Linux and Darwin binaries for each release. type BinaryBundle struct { // +kubebuilder:validation:Required // EKS Anywhere Linux binary @@ -103,6 +104,7 @@ type BinaryBundle struct { DarwinBinary Archive `json:"darwin"` } +// PlatformBundle defines the EKS Anywhere binaries by operating system. type PlatformBundle struct { // +kubebuilder:validation:Required // EKS Anywhere Linux binary @@ -113,6 +115,7 @@ type PlatformBundle struct { DarwinBinary ArchitectureBundle `json:"darwin"` } +// ArchitectureBundle defines the per-architecture binaries for an operating system. type ArchitectureBundle struct { Amd64 Archive `json:"amd64"` Arm64 Archive `json:"arm64"` diff --git a/release/cli/cmd/release.go b/release/cli/cmd/release.go index 5bcc9375eb2b..5f8f72c98393 100644 --- a/release/cli/cmd/release.go +++ b/release/cli/cmd/release.go @@ -162,12 +162,12 @@ var releaseCmd = &cobra.Command{ if devRelease { buildNumber, err := filereader.GetNextEksADevBuildNumber(releaseVersion, releaseConfig) if err != nil { - fmt.Printf("Error getting previous EKS-A dev release number: %v\n", err) + fmt.Printf("Error getting next EKS-A dev release build number: %v\n", err) os.Exit(1) } releaseVersion, err = filereader.GetCurrentEksADevReleaseVersion(releaseVersion, releaseConfig, buildNumber) if err != nil { - fmt.Printf("Error getting previous EKS-A dev release number: %v\n", err) + fmt.Printf("Error getting current EKS-A dev release version: %v\n", err) os.Exit(1) } releaseConfig.BundleNumber = buildNumber @@ -224,6 +224,12 @@ var releaseCmd = &cobra.Command{ os.Exit(1) } + err = operations.SignBundleManifest(context.Background(), bundle) + if err != nil { + fmt.Printf("Error signing bundles manifest: %+v\n", err) + os.Exit(1) + } + bundleManifest, err := yaml.Marshal(bundle) if err != nil { fmt.Printf("Error marshaling bundles manifest: %+v\n", err) diff --git a/release/cli/go.mod b/release/cli/go.mod index fe95bbd9e956..ab2253435a1c 100644 --- a/release/cli/go.mod +++ b/release/cli/go.mod @@ -4,12 +4,15 @@ go 1.22.4 require ( github.com/aws/aws-sdk-go v1.54.12 - github.com/aws/aws-sdk-go-v2 v1.30.1 + github.com/aws/aws-sdk-go-v2 v1.32.7 + github.com/aws/aws-sdk-go-v2/config v1.26.6 + github.com/aws/aws-sdk-go-v2/service/kms v1.37.9 github.com/aws/eks-anywhere v0.18.0 github.com/aws/eks-distro-build-tooling/release v0.0.0-20211103003257-a7e2379eae5e github.com/fsouza/go-dockerclient v1.11.0 github.com/ghodss/yaml v1.0.0 github.com/go-logr/logr v1.4.2 + github.com/itchyny/gojq v0.12.17 github.com/mitchellh/go-homedir v1.1.0 github.com/onsi/gomega v1.34.1 github.com/pkg/errors v0.9.1 @@ -36,7 +39,17 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/hcsshim v0.11.4 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect - github.com/aws/smithy-go v1.20.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.4 // indirect + github.com/aws/smithy-go v1.22.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect @@ -85,6 +98,7 @@ require ( github.com/huandu/xstrings v1.4.0 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -97,8 +111,8 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -124,6 +138,7 @@ require ( github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rubenv/sql-migrate v1.5.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect diff --git a/release/cli/go.sum b/release/cli/go.sum index 950d0c3d878a..35ad26ac774a 100644 --- a/release/cli/go.sum +++ b/release/cli/go.sum @@ -58,12 +58,36 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:o github.com/aws/aws-sdk-go v1.38.40/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.54.12 h1:xPDB+GSBZq0rJbmDZF+EyfMbnWRyfEPcn7PZ7bJjXSw= github.com/aws/aws-sdk-go v1.54.12/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.30.1 h1:4y/5Dvfrhd1MxRDD77SrfsDaj8kUkkljU7XE83NPV+o= -github.com/aws/aws-sdk-go-v2 v1.30.1/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= +github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o= +github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.7 h1:WJd+ubWKoBeRh7A5iNMnxEOs982SyVKOJD+K8HIezu4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.7/go.mod h1:UQi7LMR0Vhvs+44w5ec8Q+VS+cd10cjwgHwiVkE0YGU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3 h1:p+y7FvkK2dxS+FEwRIDHDe//ZX+jDhP8HHE50ppj4iI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3/go.mod h1:/fYB+FZbDlwlAiynK9KDXlzZl3ANI9JkD0Uhz5FjNT4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 h1:I9zMeF107l0rJrpnHpjEiiTSCKYAIw8mALiXcPsGBiA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15/go.mod h1:9xWJ3Q/S6Ojusz1UIkfycgD1mGirJfLLKqq3LPT7WN8= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.9 h1:Ns4iL+x1XB1SATmwAFzuebrn/d/7v8l4XDSR1/rEmJg= +github.com/aws/aws-sdk-go-v2/service/kms v1.37.9/go.mod h1:ANs9kBhK4Ghj9z1W+bsr3WsNaPF71qkgd6eE6Ekol/Y= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.2 h1:XOPfar83RIRPEzfihnp+U6udOveKZJvPQ76SKWrLRHc= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.2/go.mod h1:Vv9Xyk1KMHXrR3vNQe8W5LMFdTjSeWk0gBZBzvf3Qa0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2 h1:pi0Skl6mNl2w8qWZXcdOyg197Zsf4G97U7Sso9JXGZE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2/go.mod h1:JYzLoEVeLXk+L4tn1+rrkfhkxl6mLDEVaDSvGq9og90= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.4 h1:Ppup1nVNAOWbBOrcoOxaxPeEnSFB2RnnQdguhXpmeQk= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.4/go.mod h1:+K1rNPVyGxkRuv9NNiaZ4YhBFuyw2MMA9SlIJ1Zlpz8= github.com/aws/eks-distro-build-tooling/release v0.0.0-20211103003257-a7e2379eae5e h1:GB6Cn9yKEt31mDF7RrVWyM9WoppNkGYth8zBPIJGJ+w= github.com/aws/eks-distro-build-tooling/release v0.0.0-20211103003257-a7e2379eae5e/go.mod h1:p/KHVJAMv3kofnUnShkZ6pUnZYzm+LK2G7bIi8nnTKA= -github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= -github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -359,6 +383,10 @@ github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= +github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -426,11 +454,11 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -540,6 +568,9 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -755,6 +786,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/release/cli/pkg/clients/clients.go b/release/cli/pkg/clients/clients.go index 347ed9fba6ed..0c1e4caa7e2b 100644 --- a/release/cli/pkg/clients/clients.go +++ b/release/cli/pkg/clients/clients.go @@ -15,8 +15,11 @@ package clients import ( + "context" "fmt" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" ecrsdk "github.com/aws/aws-sdk-go/service/ecr" @@ -28,6 +31,7 @@ import ( "github.com/aws/eks-anywhere/release/cli/pkg/aws/ecr" "github.com/aws/eks-anywhere/release/cli/pkg/aws/ecrpublic" + "github.com/aws/eks-anywhere/release/cli/pkg/constants" ) type SourceClients struct { @@ -334,3 +338,14 @@ func CreateProdReleaseClients() (*SourceClients, *ReleaseClients, error) { return sourceClients, releaseClients, nil } + +// Function to create KMS client for bundle manifest signing. +func CreateKMSClient(ctx context.Context) (*kms.Client, error) { + conf, err := config.LoadDefaultConfig(ctx, config.WithRegion(constants.DefaultRegion)) + if err != nil { + return nil, fmt.Errorf("loading AWS config in region %q: %v", constants.DefaultRegion, err) + } + client := kms.NewFromConfig(conf) + + return client, nil +} diff --git a/release/cli/pkg/constants/constants.go b/release/cli/pkg/constants/constants.go index 194068793f4c..820f322f0648 100644 --- a/release/cli/pkg/constants/constants.go +++ b/release/cli/pkg/constants/constants.go @@ -54,6 +54,21 @@ const ( // (January 2, 15:04:05, 2006, in time zone seven hours west of GMT). YYYYMMDD = "2006-01-02" - MAX_IMAGES_PER_REPOSITORY = 10000 - MAX_TAGS_PER_IMAGE = 1000 + MaxImagesPerRepository = 10000 + MaxTagsPerImage = 1000 + + // Default region used to create KMS client + DefaultRegion = "us-west-2" + + // KMS key alias + KmsKey = "arn:aws:kms:us-west-2:857151390494:alias/signingEKSABundlesKey" + + // Annotations applied to the bundle during bundle manifest signing + SignatureAnnotation = "anywhere.eks.amazonaws.com/signature" + ExcludesAnnotation = "anywhere.eks.amazonaws.com/excludes" + + // Excludes is a base64-encoded, newline-delimited list of JSON/YAML paths to remove + // from the Bundles manifest prior to computing the digest. You can add or remove + // fields depending on your signing requirements. + Excludes = "LnNwZWMudmVyc2lvbnNCdW5kbGVzW10uYm9vdHN0cmFwCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmJvdHRsZXJvY2tldEhvc3RDb250YWluZXJzCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNlcnRNYW5hZ2VyCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNpbGl1bQouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5jbG91ZFN0YWNrCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNsdXN0ZXJBUEkKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uY29udHJvbFBsYW5lCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmRvY2tlcgouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5la3NhCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmV0Y2RhZG1Cb290c3RyYXAKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uZXRjZGFkbUNvbnRyb2xsZXIKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uZmx1eAouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5oYXByb3h5Ci5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmtpbmRuZXRkCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLm51dGFuaXgKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10ucGFja2FnZUNvbnRyb2xsZXIKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uc25vdwouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS50aW5rZXJiZWxsCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLnVwZ3JhZGVyCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLnZTcGhlcmU=" ) diff --git a/release/cli/pkg/images/images.go b/release/cli/pkg/images/images.go index 4517aada973b..13a6d8b6b53c 100644 --- a/release/cli/pkg/images/images.go +++ b/release/cli/pkg/images/images.go @@ -412,16 +412,16 @@ func CheckRepositoryImagesAndTagsCountLimit(sourceImageUri, releaseImageUri, sou if err != nil { return errors.Cause(err) } - if allImagesCount >= constants.MAX_IMAGES_PER_REPOSITORY { - return fmt.Errorf("cannot push image [%s] since the repository %s already has the maximum allowed number of images which is '%d'", releaseImageUri, repository, constants.MAX_IMAGES_PER_REPOSITORY) + if allImagesCount >= constants.MaxImagesPerRepository { + return fmt.Errorf("cannot push image [%s] since the repository %s already has the maximum allowed number of images which is '%d'", releaseImageUri, repository, constants.MaxImagesPerRepository) } tagsForImageCount, err := ecrpublic.GetTagsCountForImage(repository, sourceImageDigest, ecrPublicClient) if err != nil { return errors.Cause(err) } - if tagsForImageCount >= constants.MAX_TAGS_PER_IMAGE { - return fmt.Errorf("cannot push image [%s] since the image with digest [%s] in repository %s already has the maximum allowed number of tags per image which is '%d'", releaseImageUri, sourceImageDigest, repository, constants.MAX_TAGS_PER_IMAGE) + if tagsForImageCount >= constants.MaxTagsPerImage { + return fmt.Errorf("cannot push image [%s] since the image with digest [%s] in repository %s already has the maximum allowed number of tags per image which is '%d'", releaseImageUri, sourceImageDigest, repository, constants.MaxTagsPerImage) } return nil diff --git a/release/cli/pkg/operations/bundle_release.go b/release/cli/pkg/operations/bundle_release.go index 90e3e093f592..1f491cc516ff 100644 --- a/release/cli/pkg/operations/bundle_release.go +++ b/release/cli/pkg/operations/bundle_release.go @@ -30,6 +30,7 @@ import ( "github.com/aws/eks-anywhere/release/cli/pkg/constants" "github.com/aws/eks-anywhere/release/cli/pkg/filereader" "github.com/aws/eks-anywhere/release/cli/pkg/images" + sig "github.com/aws/eks-anywhere/release/cli/pkg/signature" releasetypes "github.com/aws/eks-anywhere/release/cli/pkg/types" artifactutils "github.com/aws/eks-anywhere/release/cli/pkg/util/artifacts" commandutils "github.com/aws/eks-anywhere/release/cli/pkg/util/command" @@ -222,6 +223,31 @@ func GenerateBundleSpec(r *releasetypes.ReleaseConfig, bundle *anywherev1alpha1. return nil } +// SignBundleManifest is the top-level function that computes the Bundles +// manifest signature using AWS KMS and attaches that signature as an +// annotation on the Bundles object. +func SignBundleManifest(ctx context.Context, bundle *anywherev1alpha1.Bundles) error { + fmt.Println("\n==========================================================") + fmt.Println(" Bundles Manifest Signing") + fmt.Println("==========================================================") + + if bundle.Annotations == nil { + bundle.Annotations = make(map[string]string, 1) + } + bundle.Annotations[constants.ExcludesAnnotation] = constants.Excludes + + fmt.Printf("Generating bundle signature with key: %s\n", constants.KmsKey) + + signature, err := sig.GetBundleSignature(ctx, bundle, constants.KmsKey) + if err != nil { + return err + } + bundle.Annotations[constants.SignatureAnnotation] = signature + + fmt.Printf("%s Successfully signed bundle manifest\n", constants.SuccessIcon) + return nil +} + func getImageDigest(_ context.Context, r *releasetypes.ReleaseConfig, artifact releasetypes.Artifact) (string, error) { var imageDigest string var err error diff --git a/release/cli/pkg/signature/manifest.go b/release/cli/pkg/signature/manifest.go new file mode 100644 index 000000000000..e3bc1f7c4962 --- /dev/null +++ b/release/cli/pkg/signature/manifest.go @@ -0,0 +1,171 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 signature + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "text/template" + + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/kms/types" + "github.com/itchyny/gojq" + "sigs.k8s.io/yaml" + + anywherev1alpha1 "github.com/aws/eks-anywhere/release/api/v1alpha1" + "github.com/aws/eks-anywhere/release/cli/pkg/clients" + "github.com/aws/eks-anywhere/release/cli/pkg/constants" +) + +// AlwaysExcluded are fields we always exclude from signature generation. +var AlwaysExcluded = []string{ + ".status", + ".metadata.creationTimestamp", + ".metadata.annotations", +} + +// GojqTemplate is used to build a gojq filter expression that deletes the desired fields. +var GojqTemplate = template.Must(template.New("gojq_query").Funcs( + template.FuncMap{ + "StringsJoin": strings.Join, + "Escape": func(in string) string { + // We need to escape '.' for certain gojq path usage + // to avoid ambiguities in the path expressions. + return strings.ReplaceAll(in, ".", "\\\\.") + }, + }, +).Parse(` +del({{ StringsJoin .Excludes ", " }}) +`)) + +// GetBundleSignature calls KMS and retrieves a signature, then base64-encodes it +// to store in the Bundles manifest annotation. +func GetBundleSignature(ctx context.Context, bundle *anywherev1alpha1.Bundles, key string) (string, error) { + // Compute the digest from the Bundles manifest after excluding certain fields. + digest, _, err := getDigest(bundle) + if err != nil { + return "", fmt.Errorf("computing digest: %v", err) + } + + // Create KMS Client for bundle manifest signing + kmsClient, err := clients.CreateKMSClient(ctx) + if err != nil { + return "", fmt.Errorf("creating kms client: %v", err) + } + + // The KMS service expects you to send the raw hash in the `Message` field + // when using `MessageType: DIGEST`. + input := &kms.SignInput{ + KeyId: &key, + Message: digest[:], + MessageType: types.MessageTypeDigest, + SigningAlgorithm: types.SigningAlgorithmSpecEcdsaSha256, + } + out, err := kmsClient.Sign(ctx, input) + if err != nil { + return "", fmt.Errorf("signing bundle with KMS Sign API: %v", err) + } + + // Return the base64-encoded signature bytes. + return base64.StdEncoding.EncodeToString(out.Signature), nil +} + +// getDigest converts the Bundles manifest to JSON, excludes certain fields, then +// computes the SHA256 hash of the filtered manifest. It returns the digest and +// the final bytes used to produce that digest. +func getDigest(bundle *anywherev1alpha1.Bundles) ([32]byte, []byte, error) { + var zero [32]byte + + // Marshal Bundles object to YAML + yamlBytes, err := yaml.Marshal(bundle) + if err != nil { + return zero, nil, fmt.Errorf("marshalling bundle to YAML: %v", err) + } + + // Convert YAML to JSON for easier gojq processing + jsonBytes, err := yaml.YAMLToJSON(yamlBytes) + if err != nil { + return zero, nil, fmt.Errorf("converting YAML to JSON: %v", err) + } + + // Build and execute the gojq filter that deletes excluded fields + filtered, err := filterExcludes(jsonBytes) + if err != nil { + return zero, nil, fmt.Errorf("filtering excluded fields: %v", err) + } + + // Compute the SHA256 sum of the filtered JSON + digest := sha256.Sum256(filtered) + + return digest, filtered, nil +} + +// filterExcludes applies the default and user-specified excludes to the JSON +// representation of the Bundles object using gojq. +func filterExcludes(jsonBytes []byte) ([]byte, error) { + // Decode the base64-encoded excludes + exclBytes, err := base64.StdEncoding.DecodeString(constants.Excludes) + if err != nil { + return nil, fmt.Errorf("decoding Excludes: %v", err) + } + // Convert them into slice of strings + userExcludes := strings.Split(string(exclBytes), "\n") + + // Combine AlwaysExcluded with userExcludes + allExcludes := append(AlwaysExcluded, userExcludes...) + + // Build the argument to the gojq template + var tmplBuf bytes.Buffer + if err := GojqTemplate.Execute(&tmplBuf, map[string]interface{}{ + "Excludes": allExcludes, + }); err != nil { + return nil, fmt.Errorf("executing gojq template: %v", err) + } + + // Parse the final gojq query + query, err := gojq.Parse(tmplBuf.String()) + if err != nil { + return nil, fmt.Errorf("gojq parse error: %v", err) + } + + // Unmarshal the JSON into a generic interface so gojq can operate + var input interface{} + if err := json.Unmarshal(jsonBytes, &input); err != nil { + return nil, fmt.Errorf("unmarshalling JSON: %v", err) + } + + // Run the query + iter := query.Run(input) + finalVal, ok := iter.Next() + if !ok { + return nil, errors.New("gojq produced no result") + } + if errVal, ok := finalVal.(error); ok { + return nil, fmt.Errorf("gojq execution error: %v", errVal) + } + + // Marshal the filtered result back to JSON + filtered, err := json.Marshal(finalVal) + if err != nil { + return nil, fmt.Errorf("marshalling final result to JSON: %v", err) + } + return filtered, nil +} diff --git a/release/cli/pkg/signature/manifest_test.go b/release/cli/pkg/signature/manifest_test.go new file mode 100644 index 000000000000..58f78891dbe7 --- /dev/null +++ b/release/cli/pkg/signature/manifest_test.go @@ -0,0 +1,298 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 signature + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + anywherev1alpha1 "github.com/aws/eks-anywhere/release/api/v1alpha1" + "github.com/aws/eks-anywhere/release/cli/pkg/constants" +) + +func TestGetBundleSignature(t *testing.T) { + testCases := []struct { + testName string + bundle *anywherev1alpha1.Bundles + key string + expectErrSubstr string + }{ + { + testName: "Nil bundle", + bundle: nil, + key: constants.KmsKey, + expectErrSubstr: "computing digest:", + }, + { + testName: "Excluding fields from bundle with minimal valid data", + bundle: &anywherev1alpha1.Bundles{ + Spec: anywherev1alpha1.BundlesSpec{ + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.31", + }, + }, + }, + }, + key: constants.KmsKey, + expectErrSubstr: "", + }, + { + testName: "Excluding fields from a fully populated Bundles object", + bundle: &anywherev1alpha1.Bundles{ + Spec: anywherev1alpha1.BundlesSpec{ + Number: 10, + CliMinVersion: "v1.0.0", + CliMaxVersion: "v2.0.0", + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.28", + EndOfStandardSupport: "2024-10-10", + EksD: anywherev1alpha1.EksDRelease{ + Name: "eks-d-1-25", + ReleaseChannel: "1-25", + KubeVersion: "1.25", + EksDReleaseUrl: "https://example.com/release.yaml", + }, + CertManager: anywherev1alpha1.CertManagerBundle{ + Version: "v1.11.0", + Acmesolver: anywherev1alpha1.Image{ + URI: "public.ecr.aws/acmesolver:latest", + }, + Cainjector: anywherev1alpha1.Image{ + URI: "public.ecr.aws/cainjector:latest", + }, + Controller: anywherev1alpha1.Image{ + URI: "public.ecr.aws/cert-manager-controller:latest", + }, + Startupapicheck: anywherev1alpha1.Image{ + URI: "public.ecr.aws/startupapicheck:latest", + }, + Webhook: anywherev1alpha1.Image{ + URI: "public.ecr.aws/webhook:latest", + }, + Manifest: anywherev1alpha1.Manifest{ + URI: "https://example.com/cert-manager.yaml", + }, + }, + Eksa: anywherev1alpha1.EksaBundle{ + Version: "v0.0.1-dev", + CliTools: anywherev1alpha1.Image{ + URI: "public.ecr.aws/eks-anywhere-cli-tools:latest", + }, + }, + }, + { + KubeVersion: "1.26", + }, + }, + }, + }, + key: constants.KmsKey, + expectErrSubstr: "", + }, + } + + for _, tt := range testCases { + t.Run(tt.testName, func(t *testing.T) { + g := NewWithT(t) + + ctx := context.Background() + sig, err := GetBundleSignature(ctx, tt.bundle, tt.key) + + if tt.expectErrSubstr == "" { + // Expecting no particular error substring -> test for success + g.Expect(err).NotTo(HaveOccurred(), + "Expected no error but got error: %v", err) + g.Expect(sig).NotTo(BeEmpty(), + "Expected signature string to be non-empty on success") + } else { + // Expecting an error substring -> test for error presence + g.Expect(err).To(HaveOccurred(), + "Expected an error but got none") + g.Expect(err.Error()).To(ContainSubstring(tt.expectErrSubstr), + "Error message should contain substring %q, got: %v", tt.expectErrSubstr, err) + g.Expect(sig).To(BeEmpty(), + "Expected signature to be empty when error occurs") + } + }) + } +} + +func TestGetDigest(t *testing.T) { + testCases := []struct { + testName string + bundle *anywherev1alpha1.Bundles + expectErrSubstr string + }{ + { + testName: "Simple valid bundle", + bundle: &anywherev1alpha1.Bundles{ + Spec: anywherev1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.31", + }, + }, + }, + }, + expectErrSubstr: "", + }, + { + testName: "Another valid bundle with more fields", + bundle: &anywherev1alpha1.Bundles{ + Spec: anywherev1alpha1.BundlesSpec{ + Number: 10, + CliMinVersion: "v0.0.1", + VersionsBundles: []anywherev1alpha1.VersionsBundle{ + { + KubeVersion: "1.28", + }, + { + KubeVersion: "1.29", + }, + }, + }, + }, + expectErrSubstr: "", + }, + } + + for _, tt := range testCases { + t.Run(tt.testName, func(t *testing.T) { + g := NewWithT(t) + + digest, filtered, err := getDigest(tt.bundle) + if tt.expectErrSubstr == "" { + g.Expect(err).NotTo(HaveOccurred(), "Expected success but got error") + g.Expect(digest).NotTo(BeZero(), + "Expected digest to be non-zero array") + g.Expect(filtered).NotTo(BeEmpty(), + "Expected filtered bytes to be non-empty") + } else { + g.Expect(err).To(HaveOccurred(), + "Expected error but got none") + g.Expect(err.Error()).To(ContainSubstring(tt.expectErrSubstr), + "Error message should contain substring %q, got: %v", + tt.expectErrSubstr, err) + g.Expect(digest).To(BeZero()) + g.Expect(filtered).To(BeNil()) + } + }) + } +} + +func TestFilterExcludes(t *testing.T) { + testCases := []struct { + testName string + jsonPayload string + expectErrSubstr string + expectExclude []string // substrings we expect to NOT be present + expectInclude []string // substrings we expect to be present + }{ + { + testName: "Valid JSON with known excludes", + jsonPayload: `{ + "metadata": { + "creationTimestamp": "2021-09-01T00:00:00Z", + "annotations": { "key": "value" } + }, + "status": { + "someStatus": "info" + }, + "spec": { + "versionsBundles": [{ + "kubeVersion": "1.28", + "endOfExtendedSupport": "2024-12-31", + "eksD": { + "channel": "1-28", + "components": "https://distro.eks.amazonaws.com/crds/releases.distro.eks.amazonaws.com-v1alpha1.yaml", + "gitCommit": "3c3ff5d3aaa7417b906549756da44f60af5df03d", + "kubeVersion": "v1.28.15", + "manifestUrl": "https://distro.eks.amazonaws.com/kubernetes-1-28/kubernetes-1-28-eks-37.yaml", + "name": "kubernetes-1-28-eks-37" + }, + "eksa": "someValue" + }], + "otherField": "otherValue" + } + }`, + expectErrSubstr: "", + expectExclude: []string{ + "creationTimestamp", + "annotations", + "status", + "eksa", + }, + expectInclude: []string{ + "kubeVersion", + "endOfExtendedSupport", + "eksD", + }, + }, + { + testName: "Invalid JSON payload", + jsonPayload: `{"unclosed": [`, + expectErrSubstr: "unmarshalling JSON:", + }, + { + testName: "Excludes with minimal JSON", + jsonPayload: `{ + "metadata": {"creationTimestamp": "2021-09-01T00:00:00Z"}, + "spec": { + "versionsBundles": [{ + "kubeVersion": "1.31" + }] + } + }`, + expectErrSubstr: "", + expectExclude: []string{"creationTimestamp"}, + expectInclude: []string{"spec"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.testName, func(t *testing.T) { + g := NewWithT(t) + + filtered, err := filterExcludes([]byte(tt.jsonPayload)) + + if tt.expectErrSubstr == "" { + g.Expect(err).NotTo(HaveOccurred(), + "Expected success but got error: %v", err) + g.Expect(filtered).NotTo(BeEmpty(), "Expected non-empty filtered output") + + // Convert filtered output back to string for substring checks + filteredStr := string(filtered) + for _, excl := range tt.expectExclude { + g.Expect(filteredStr).NotTo(ContainSubstring(excl), + "Expected %q to be excluded but it was present", excl) + } + for _, incl := range tt.expectInclude { + g.Expect(filteredStr).To(ContainSubstring(incl), + "Expected %q to be included but it was not found", incl) + } + } else { + g.Expect(err).To(HaveOccurred(), + "Expected error but got none") + g.Expect(err.Error()).To(ContainSubstring(tt.expectErrSubstr), + "Error should contain substring %q", tt.expectErrSubstr) + } + }) + } +}