diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go index 719f4bbd2915..a854a9d54473 100644 --- a/controllers/cluster_controller.go +++ b/controllers/cluster_controller.go @@ -649,7 +649,7 @@ func validateExtendedK8sVersionSupport(ctx context.Context, client client.Client cluster.Status.FailureReason = &reason return fmt.Errorf("getting bundle for cluster: %w", err) } - if err = validations.ValidateExtendedK8sVersionSupport(ctx, cluster, bundle, clientutil.NewKubeClient(client)); err != nil { + if err = validations.ValidateExtendedK8sVersionSupport(ctx, *cluster, bundle, clientutil.NewKubeClient(client)); err != nil { reason := anywherev1.ExtendedK8sVersionSupportNotSupportedReason cluster.Status.FailureMessage = ptr.String(err.Error()) cluster.Status.FailureReason = &reason diff --git a/controllers/cluster_controller_test.go b/controllers/cluster_controller_test.go index 8fdf5333012a..dae51dfc6fa6 100644 --- a/controllers/cluster_controller_test.go +++ b/controllers/cluster_controller_test.go @@ -151,7 +151,8 @@ func TestClusterReconcilerReconcileSelfManagedCluster(t *testing.T) { Name: "my-management-cluster", }, Spec: anywherev1.ClusterSpec{ - EksaVersion: &version, + KubernetesVersion: anywherev1.Kube132, + EksaVersion: &version, ClusterNetwork: anywherev1.ClusterNetwork{ CNIConfig: &anywherev1.CNIConfig{ Cilium: &anywherev1.CiliumConfig{}, @@ -1082,6 +1083,7 @@ func TestClusterReconcilerReconcileSelfManagedClusterRegAuthFailNoSecret(t *test Name: "my-management-cluster", }, Spec: anywherev1.ClusterSpec{ + KubernetesVersion: anywherev1.Kube132, ClusterNetwork: anywherev1.ClusterNetwork{ CNIConfig: &anywherev1.CNIConfig{ Cilium: &anywherev1.CiliumConfig{}, @@ -1252,7 +1254,7 @@ func TestClusterReconcilerSkipDontInstallPackagesOnSelfManaged(t *testing.T) { Namespace: "default", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, ClusterNetwork: anywherev1.ClusterNetwork{ CNIConfig: &anywherev1.CNIConfig{ Cilium: &anywherev1.CiliumConfig{}, @@ -1402,7 +1404,7 @@ func TestClusterReconcilerPackagesInstall(s *testing.T) { Namespace: "default", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, ClusterNetwork: anywherev1.ClusterNetwork{ CNIConfig: &anywherev1.CNIConfig{ Cilium: &anywherev1.CiliumConfig{}, @@ -1667,17 +1669,17 @@ func createBundle() *releasev1.Bundles { Name: "bundles-1", Namespace: "default", Annotations: map[string]string{ - constants.SignatureAnnotation: "MEUCIQDbVAB+yy+pdCOFet/vWMoHQA2FYiiQtq1zltBRRhRo2QIgGQopCHraD/HpvpSh4Q7rVdesXeVriJv2ucEnoidoZlg=", + constants.SignatureAnnotation: "MEQCIG8DZfnqQtx1fF5x2assfSUEvuJ9BqaCN8jaoBHxKU8SAiBwR2B/T2BC3nzmnT2uEvwyemOy+A7V/K+PkGuKGX0E1Q==", }, }, Spec: releasev1.BundlesSpec{ VersionsBundles: []releasev1.VersionsBundle{ { - KubeVersion: "1.30", + KubeVersion: "1.32", EksD: releasev1.EksDRelease{ Name: "test", EksDReleaseUrl: "testdata/release.yaml", - KubeVersion: "1.30", + KubeVersion: "1.32", }, CertManager: releasev1.CertManagerBundle{}, ClusterAPI: releasev1.CoreClusterAPI{}, @@ -1693,6 +1695,7 @@ func createBundle() *releasev1.Bundles { ExternalEtcdBootstrap: releasev1.EtcdadmBootstrapBundle{}, ExternalEtcdController: releasev1.EtcdadmControllerBundle{}, Tinkerbell: releasev1.TinkerbellBundle{}, + EndOfStandardSupport: "2030-06-30", }, }, }, @@ -1745,7 +1748,7 @@ func vsphereCluster() *anywherev1.Cluster { Kind: "VSphereDatacenterConfig", Name: "datacenter", }, - KubernetesVersion: "1.20", + KubernetesVersion: anywherev1.Kube132, ControlPlaneConfiguration: anywherev1.ControlPlaneConfiguration{ Count: 1, Endpoint: &anywherev1.Endpoint{ @@ -1864,26 +1867,7 @@ func baseTestVsphereCluster() (*cluster.Config, *releasev1.Bundles) { config.AWSIAMConfigs[awsIAM.Name] = awsIAM config.OIDCConfigs[oidc.Name] = oidc - bundles := &releasev1.Bundles{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-bundles-ref", - Namespace: config.Cluster.Namespace, - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEYCIQDA40Bizd/0mdCwRCIKq10gjLdJMT0s0y57RPW/zOyWZwIhALPOFS+NZZ7QCwI7wiC1TiArMHMq4TbzIJcx85H/zjU4", - }, - }, - Spec: releasev1.BundlesSpec{ - VersionsBundles: []releasev1.VersionsBundle{ - { - KubeVersion: "v1.30", - PackageController: releasev1.PackageBundle{ - HelmChart: releasev1.Image{}, - }, - }, - }, - }, - } - + bundles := createBundle() config.Cluster.Spec.BundlesRef = &anywherev1.BundlesRef{ Name: bundles.Name, Namespace: bundles.Namespace, diff --git a/controllers/cluster_controller_test_test.go b/controllers/cluster_controller_test_test.go index 73c00e4cf607..4f87a0d2ced6 100644 --- a/controllers/cluster_controller_test_test.go +++ b/controllers/cluster_controller_test_test.go @@ -27,7 +27,6 @@ import ( "github.com/aws/eks-anywhere/pkg/controller" "github.com/aws/eks-anywhere/pkg/controller/clientutil" "github.com/aws/eks-anywhere/pkg/controller/clusters" - "github.com/aws/eks-anywhere/release/api/v1alpha1" ) func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { @@ -54,7 +53,7 @@ func TestClusterReconcilerEnsureOwnerReferences(t *testing.T) { Namespace: "default", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, EksaVersion: &version, }, Status: anywherev1.ClusterStatus{ @@ -244,8 +243,8 @@ func TestClusterReconcilerSetBundlesRef(t *testing.T) { }, Spec: anywherev1.ClusterSpec{ BundlesRef: &anywherev1.BundlesRef{ - Name: "my-bundles-ref", - Namespace: "my-namespace", + Name: "bundles-1", + Namespace: "default", }, }, Status: anywherev1.ClusterStatus{ @@ -258,10 +257,10 @@ func TestClusterReconcilerSetBundlesRef(t *testing.T) { Name: "my-cluster", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, BundlesRef: &anywherev1.BundlesRef{ - Name: "my-bundles-ref", - Namespace: "my-namespace", + Name: "bundles-1", + Namespace: "default", }, }, Status: anywherev1.ClusterStatus{ @@ -275,25 +274,7 @@ func TestClusterReconcilerSetBundlesRef(t *testing.T) { Namespace: constants.EksaSystemNamespace, }, } - bundles := &v1alpha1.Bundles{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-bundles-ref", - Namespace: cluster.Spec.BundlesRef.Namespace, - Annotations: map[string]string{ - constants.SignatureAnnotation: "MEYCIQDA40Bizd/0mdCwRCIKq10gjLdJMT0s0y57RPW/zOyWZwIhALPOFS+NZZ7QCwI7wiC1TiArMHMq4TbzIJcx85H/zjU4", - }, - }, - Spec: v1alpha1.BundlesSpec{ - VersionsBundles: []v1alpha1.VersionsBundle{ - { - KubeVersion: "v1.30", - PackageController: v1alpha1.PackageBundle{ - HelmChart: v1alpha1.Image{}, - }, - }, - }, - }, - } + bundles := createBundle() objs := []runtime.Object{cluster, managementCluster, secret, bundles} cb := fake.NewClientBuilder() @@ -347,7 +328,7 @@ func TestClusterReconcilerSetDefaultEksaVersion(t *testing.T) { Namespace: "default", }, Spec: anywherev1.ClusterSpec{ - KubernetesVersion: "v1.25", + KubernetesVersion: anywherev1.Kube132, }, Status: anywherev1.ClusterStatus{ ReconciledGeneration: 1, diff --git a/go.mod b/go.mod index b56dcd41c917..3fbaf433365d 100644 --- a/go.mod +++ b/go.mod @@ -124,6 +124,7 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/gobuffalo/flect v1.0.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect diff --git a/go.sum b/go.sum index 3ec736dc5680..6b4355d8ac17 100644 --- a/go.sum +++ b/go.sum @@ -437,7 +437,10 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 9c184cc0d60d..d91c55d2a81b 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -122,6 +122,8 @@ const ( Excludes = "LnNwZWMudmVyc2lvbnNCdW5kbGVzW10uYm9vdHN0cmFwCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmJvdHRsZXJvY2tldEhvc3RDb250YWluZXJzCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNlcnRNYW5hZ2VyCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNpbGl1bQouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5jbG91ZFN0YWNrCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmNsdXN0ZXJBUEkKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uY29udHJvbFBsYW5lCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmRvY2tlcgouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5la3NhCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmV0Y2RhZG1Cb290c3RyYXAKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uZXRjZGFkbUNvbnRyb2xsZXIKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uZmx1eAouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS5oYXByb3h5Ci5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLmtpbmRuZXRkCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLm51dGFuaXgKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10ucGFja2FnZUNvbnRyb2xsZXIKLnNwZWMudmVyc2lvbnNCdW5kbGVzW10uc25vdwouc3BlYy52ZXJzaW9uc0J1bmRsZXNbXS50aW5rZXJiZWxsCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLnVwZ3JhZGVyCi5zcGVjLnZlcnNpb25zQnVuZGxlc1tdLnZTcGhlcmU=" // KMSPublicKey to verify bundle signature. KMSPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFZU/Z6VVMU9HioT7rGkPdJg3frC2xyQZhWFIrz5HeZEfeQ2nAdnJMLrs2Qr3V9xVrJrHA54wnIHDoPGbEhojqg==" + // LincesePublicKey is to verify the licenseKey field. + LincesePublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE96Xb67YUq+at8gFOioYlf1kxOIPio7i3Y8sFrG3a3sn/MzqQmTO9K82psqOuN+E4NdE8VajOtbyfcLo+Ojax/w==" ) type Operation int diff --git a/pkg/signature/manifest.go b/pkg/signature/manifest.go index e0f8434b294a..4508d17503df 100644 --- a/pkg/signature/manifest.go +++ b/pkg/signature/manifest.go @@ -26,6 +26,7 @@ import ( "strings" "text/template" + "github.com/golang-jwt/jwt/v5" "github.com/itchyny/gojq" "sigs.k8s.io/yaml" @@ -50,19 +51,9 @@ func ValidateSignature(bundle *anywherev1alpha1.Bundles, pubKey string) (valid b return false, fmt.Errorf("signature in metadata isn't base64 encoded: %w", err) } - pubdecoded, err := base64.StdEncoding.DecodeString(pubKey) + pubkey, err := parsePublicKey(pubKey) if err != nil { - return false, fmt.Errorf("decoding the public key as string: %w", err) - } - - pubparsed, err := x509.ParsePKIXPublicKey(pubdecoded) - if err != nil { - return false, fmt.Errorf("parsing the public key (not PKIX): %w", err) - } - - pubkey, ok := pubparsed.(*ecdsa.PublicKey) - if !ok { - return false, fmt.Errorf("parsing the public key (not ECDSA): %T", pubparsed) + return false, err } return ecdsa.VerifyASN1(pubkey, digest[:], sig), nil @@ -162,3 +153,45 @@ func filterExcludes(jsonBytes []byte) ([]byte, error) { } return filtered, nil } + +func parsePublicKey(key string) (*ecdsa.PublicKey, error) { + pubdecoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, fmt.Errorf("decoding the public key as string: %w", err) + } + + pubparsed, err := x509.ParsePKIXPublicKey(pubdecoded) + if err != nil { + return nil, fmt.Errorf("parsing the public key (not PKIX): %w", err) + } + + pubkey, ok := pubparsed.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("parsing the public key (not ECDSA): %T", pubparsed) + } + return pubkey, nil +} + +// ParseLicense parses licenseKey jwt token using the public key and returns token fields. +func ParseLicense(licenseToken string, key string) (*jwt.Token, error) { + tokenKey, err := parsePublicKey(key) + if err != nil { + return nil, err + } + + token, err := jwt.Parse(licenseToken, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodECDSA); !ok { + return nil, fmt.Errorf("signing method not supported: %v", t.Header["alg"]) + } + return tokenKey, nil + }) + if err != nil { + return nil, fmt.Errorf("parsing licenseToken: %w", err) + } + + if !token.Valid { + return nil, errors.New("licenseToken is not valid") + } + + return token, nil +} diff --git a/pkg/signature/manifest_test.go b/pkg/signature/manifest_test.go index 05016abea91c..37efa15db8f7 100644 --- a/pkg/signature/manifest_test.go +++ b/pkg/signature/manifest_test.go @@ -1,10 +1,17 @@ package signature import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/base64" "fmt" "strings" "testing" + "time" + "github.com/golang-jwt/jwt/v5" "github.com/onsi/gomega" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -157,7 +164,6 @@ func TestValidateSignature(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(_ *testing.T) { valid, err := ValidateSignature(tc.bundle, tc.publicKey) - fmt.Println(err) if err != nil && !strings.Contains(err.Error(), tc.wantErr.Error()) { t.Errorf("%v got = %v, \nwant %v", tc.name, err, tc.wantErr) } @@ -336,3 +342,79 @@ func TestFilterExcludes(t *testing.T) { }) } } + +func TestParseLicense(t *testing.T) { + tests := []struct { + name string + licenseKey string + key string + wantErr error + }{ + { + name: "malformed token", + licenseKey: "invalid.token.string", + key: constants.LincesePublicKey, + wantErr: fmt.Errorf("parsing licenseToken"), + }, + { + name: "invalid public key", + licenseKey: "invalid.token.string", + key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7XtGi5M5nUyoXZpZWg5e9YgQaVbUq4DbxFkGn7yM9rIg+45dQ1pJwYQd/Z9RDZ3umTZHfdmVfaMT8E/2jpa6vYh5AroOn75tN8qaGmG2OqEBoA8k84zK98qNdOJow7CcIWjHQGk6Tr/dSfdTC6ydmBdRMX/7bBYcKylOFf2P65HOMQCB5YdZJAYzvlXEXzoc1o7DD3pT68BOHHTJp6h7+GGXZoNlHJeq1+AKq38Ra6tuI8EUV2S/5+75FFJzMTLVlJ20Jlhh3fuWJtn6a2hGeD/fbZ1w6CMi0dCTGEX6wUOmL5FJ4RFSVthqZCZ7Ap0G2/5Mu3pxVR9glAxThOw61QIDAQAB", + wantErr: fmt.Errorf("parsing the public key (not ECDSA)"), + }, + { + name: "signing method not supported", + licenseKey: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleGFtcGxlIjoiZGF0YSJ9.UKzt6DArjTtHk_Nch6TwbdgVni6FwLJ1fdbVNYikE_kFGTzMZC82m_0qY7l27LtN0J6b_5D8hLLFk3pTZHYGBX5kB2XKH5e5syRkGh6uZHDkGtRjTMoD5sPMZJ0rG4m80k8cgI37UsIt66hoK_45FzSMlTwxogJ2nJk5G1dH10", + key: constants.LincesePublicKey, + wantErr: fmt.Errorf("signing method not supported"), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ParseLicense(tc.licenseKey, tc.key) + if err != nil && !strings.Contains(err.Error(), tc.wantErr.Error()) { + t.Errorf("%v got = %v, \nwant %v", tc.name, err, tc.wantErr) + } + }) + } +} + +func generateTestKeys() (string, *ecdsa.PrivateKey, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return "", nil, fmt.Errorf("failed to generate private key: %w", err) + } + publicKey := &privateKey.PublicKey + + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", nil, fmt.Errorf("failed to marshal public key: %w", err) + } + publicKeyBase64 := base64.StdEncoding.EncodeToString(publicKeyBytes) + + return publicKeyBase64, privateKey, nil +} + +func TestParseLicense_Success(t *testing.T) { + publicKeyBase64, privateKey, err := generateTestKeys() + if err != nil { + t.Errorf("Failed to generate test keys: %v", err) + } + + claims := jwt.MapClaims{ + "iss": "test", + "sub": "12345", + "exp": time.Now().Add(time.Hour * 24).Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) + signedToken, err := token.SignedString(privateKey) + if err != nil { + t.Errorf("Failed to sign token: %v", err) + } + + _, err = ParseLicense(signedToken, publicKeyBase64) + if err != nil { + t.Errorf("ParseLicense failed: %v", err) + } +} diff --git a/pkg/validations/extendedversion.go b/pkg/validations/extendedversion.go index f027033161e4..3848ec3816de 100644 --- a/pkg/validations/extendedversion.go +++ b/pkg/validations/extendedversion.go @@ -2,21 +2,41 @@ package validations import ( "context" + "errors" "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1" "github.com/aws/eks-anywhere/pkg/clients/kubernetes" + "github.com/aws/eks-anywhere/pkg/cluster" "github.com/aws/eks-anywhere/pkg/constants" "github.com/aws/eks-anywhere/pkg/signature" "github.com/aws/eks-anywhere/release/api/v1alpha1" ) // ValidateExtendedK8sVersionSupport validates all the validations needed for the support of extended kubernetes support. -func ValidateExtendedK8sVersionSupport(_ context.Context, _ *anywherev1.Cluster, bundle *v1alpha1.Bundles, _ kubernetes.Client) error { +func ValidateExtendedK8sVersionSupport(_ context.Context, clusterSpec anywherev1.Cluster, bundle *v1alpha1.Bundles, _ kubernetes.Client) error { // Validate EKS-A bundle has not been modified by verifying the signature in the bundle annotation if err := validateBundleSignature(bundle); err != nil { return fmt.Errorf("validating bundle signature: %w", err) } + + // Check whether the kubernetes version for the cluster is currently under extended support by comparing the endOfStandardSupport date from the bundle with the current date. + isExtended, err := isExtendedSupport(clusterSpec.Spec.KubernetesVersion, bundle) + if err != nil { + return err + } + if isExtended { + token, err := getLicense(clusterSpec.Spec.LicenseToken) + if err != nil { + return fmt.Errorf("getting licenseToken: %w", err) + } + if err = validateLicense(token); err != nil { + return fmt.Errorf("validating licenseToken: %w", err) + } + } return nil } @@ -31,3 +51,56 @@ func validateBundleSignature(bundle *v1alpha1.Bundles) error { } return nil } + +func isExtendedSupport(kubernetesVersion anywherev1.KubernetesVersion, bundle *v1alpha1.Bundles) (bool, error) { + versionsBundle, err := cluster.GetVersionsBundle(kubernetesVersion, bundle) + if err != nil { + return false, fmt.Errorf("getting versions bundle for %s kubernetes version: %w", kubernetesVersion, err) + } + + endOfStandardSupport, err := time.Parse("2006-01-02", versionsBundle.EndOfStandardSupport) + if err != nil { + return false, fmt.Errorf("parsing EndOfStandardSupport field format: %w", err) + } + + return isPastDateThanToday(endOfStandardSupport), nil +} + +func getLicense(licenseToken string) (*jwt.Token, error) { + if licenseToken == "" { + return nil, errors.New("licenseToken is required for extended kubernetes support") + } + token, err := signature.ParseLicense(licenseToken, constants.LincesePublicKey) + if err != nil { + return nil, fmt.Errorf("parsing licenseToken: %w", err) + } + + return token, nil +} + +func validateLicense(token *jwt.Token) error { + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return errors.New("could not parse the licenseToken claims") + } + endValidity, ok := claims["endValidity"].(string) + if !ok { + return errors.New("license validity field missing from the licenseToken, not a valid license") + } + + validity, err := time.Parse(time.RFC3339, endValidity) + if err != nil { + return fmt.Errorf("parsing endValidity field from licenseToken: %w", err) + } + + if isPastDateThanToday(validity) { + return errors.New("license is expired, please renew the license") + } + + return nil +} + +func isPastDateThanToday(dateToCompare time.Time) bool { + today := time.Now().Truncate(24 * time.Hour) + return dateToCompare.Before(today) +} diff --git a/pkg/validations/extendedversion_test.go b/pkg/validations/extendedversion_test.go index 4aa94e307423..f78d5c3d99dc 100644 --- a/pkg/validations/extendedversion_test.go +++ b/pkg/validations/extendedversion_test.go @@ -19,16 +19,17 @@ import ( func TestValidateExtendedK8sVersionSupport(t *testing.T) { ctx := context.Background() client := test.NewFakeKubeClient() + tests := []struct { name string - cluster *anywherev1.Cluster + cluster anywherev1.Cluster bundle *v1alpha1.Bundles client kubernetes.Client wantErr error }{ { - name: "No bundle signature", - cluster: &anywherev1.Cluster{}, + name: "no bundle signature", + cluster: anywherev1.Cluster{}, bundle: &v1alpha1.Bundles{ ObjectMeta: v1.ObjectMeta{ Annotations: map[string]string{ @@ -39,8 +40,22 @@ func TestValidateExtendedK8sVersionSupport(t *testing.T) { wantErr: fmt.Errorf("missing signature annotation"), }, { - name: "bundle verification succeeded", - cluster: &anywherev1.Cluster{}, + name: "kubernetes version not supported", + cluster: anywherev1.Cluster{ + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "1.22", + }, + }, + bundle: validBundle(), + wantErr: fmt.Errorf("getting versions bundle for 1.22 kubernetes version"), + }, + { + name: "unsupported EndOfStandardSupport format", + cluster: anywherev1.Cluster{ + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "1.28", + }, + }, bundle: &v1alpha1.Bundles{ TypeMeta: v1.TypeMeta{ Kind: "Bundles", @@ -48,19 +63,42 @@ func TestValidateExtendedK8sVersionSupport(t *testing.T) { }, ObjectMeta: v1.ObjectMeta{ Annotations: map[string]string{ - constants.SignatureAnnotation: "MEYCIQCiWwxw/Nchkgtan47FzagXHgB45Op7YWxvSZjFzHau8wIhALG2kbm+H8HJEfN/rUQ0ldo298MnzyhukBptUm0jCtZZ", + constants.SignatureAnnotation: "MEYCIQCYJwrDjICgUQImFpJdOLjQlC7OSQutCsqBk+0jUheZTQIhALSj7peTLSTSy9rvNfYwyqbP0fOi3elggWwPcAz89csc", }, }, Spec: v1alpha1.BundlesSpec{ Number: 1, VersionsBundles: []v1alpha1.VersionsBundle{ { - KubeVersion: "1.31", + KubeVersion: "1.28", + EndOfStandardSupport: "2024-31-12", }, }, }, }, - wantErr: fmt.Errorf("missing signature annotation"), + wantErr: fmt.Errorf("parsing EndOfStandardSupport field format"), + }, + { + name: "missing license token", + cluster: anywherev1.Cluster{ + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "1.28", + LicenseToken: "", + }, + }, + bundle: validBundle(), + wantErr: fmt.Errorf("licenseToken is required for extended kubernetes support"), + }, + { + name: "invalid licenseKey", + cluster: anywherev1.Cluster{ + Spec: anywherev1.ClusterSpec{ + KubernetesVersion: "1.28", + LicenseToken: "invalid-token", + }, + }, + bundle: validBundle(), + wantErr: fmt.Errorf("getting licenseToken"), }, } @@ -73,3 +111,26 @@ func TestValidateExtendedK8sVersionSupport(t *testing.T) { }) } } + +func validBundle() *v1alpha1.Bundles { + return &v1alpha1.Bundles{ + TypeMeta: v1.TypeMeta{ + Kind: "Bundles", + APIVersion: v1alpha1.GroupVersion.String(), + }, + ObjectMeta: v1.ObjectMeta{ + Annotations: map[string]string{ + constants.SignatureAnnotation: "MEYCIQC8Fuo81dxibtkvrOFZpbFXZGmJnhLN6bkJjx4YB0fGIQIhAJIxIAl3s26eXqcmS6kAyjDd0NXDlBbM0d/GCHcL2Xoo", + }, + }, + Spec: v1alpha1.BundlesSpec{ + Number: 1, + VersionsBundles: []v1alpha1.VersionsBundle{ + { + KubeVersion: "1.28", + EndOfStandardSupport: "2024-12-31", + }, + }, + }, + } +}