From cbf5e65e8b3160822d46673540d51f272eb4a401 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Wed, 29 Nov 2023 10:19:06 +0100 Subject: [PATCH] GT-525 License Manager for ML Deployment (#1501) --- .gitignore | 3 + CHANGELOG.md | 1 + pkg/apis/ml/v1alpha1/extension_conditions.go | 4 +- pkg/license/license.community.go | 3 +- pkg/license/license.go | 26 ++++++- pkg/license/loader.go | 3 +- pkg/license/loader_arangodeployment.go | 32 +++------ pkg/license/loader_test.go | 35 ++++++++++ pkg/util/cert/signer.go | 71 ++++++++++++++++++++ 9 files changed, 150 insertions(+), 28 deletions(-) create mode 100644 pkg/license/loader_test.go create mode 100644 pkg/util/cert/signer.go diff --git a/.gitignore b/.gitignore index 72c603532..2b66d5b04 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,13 @@ vendor/ .idea/ deps/ .vscode/ + **/*.enterprise.go +**/*.enterprise_test.go **/enterprise/** enterprise.mk license-header.enterprise.txt + local/ kustomize_test/ tools/codegen/boilerplate.go.txt \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f675a42..32874479b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - (Feature) (ML) Introduce basic Conditions - (Improvement) Raise memory requests for init containers to 50mi - (Feature) (ML) Metadata Service Implementation +- (Feature) License Manager for ML Deployment ## [1.2.35](https://github.com/arangodb/kube-arangodb/tree/1.2.35) (2023-11-06) - (Maintenance) Update go-driver to v1.6.0, update IsNotFound() checks diff --git a/pkg/apis/ml/v1alpha1/extension_conditions.go b/pkg/apis/ml/v1alpha1/extension_conditions.go index 09daa234f..4e2fca8fc 100644 --- a/pkg/apis/ml/v1alpha1/extension_conditions.go +++ b/pkg/apis/ml/v1alpha1/extension_conditions.go @@ -23,7 +23,7 @@ package v1alpha1 import api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" const ( - ExtensionDeploymentFoundCondition api.ConditionType = "DeploymentFound" - + ExtensionDeploymentFoundCondition api.ConditionType = "DeploymentFound" ExtensionMetadataServiceValidCondition api.ConditionType = "MetadataServiceValid" + LicenseValidCondition api.ConditionType = "LicenseValid" ) diff --git a/pkg/license/license.community.go b/pkg/license/license.community.go index 81340059f..4bb77f6c8 100644 --- a/pkg/license/license.community.go +++ b/pkg/license/license.community.go @@ -16,7 +16,8 @@ // limitations under the License. // // Copyright holder is ArangoDB GmbH, Cologne, Germany -// + +//go:build !enterprise package license diff --git a/pkg/license/license.go b/pkg/license/license.go index 2903430b6..32eed2940 100644 --- a/pkg/license/license.go +++ b/pkg/license/license.go @@ -55,7 +55,7 @@ const ( // NotLicensed StatusFeatureExpired - // StatusValid define state when Operator should continue execution + // StatusValid define state when Operator should continue execution // Licensed StatusValid ) @@ -70,6 +70,29 @@ func (s Status) Validate(feature Feature, subFeatures ...Feature) Status { type Feature string +const ( + // FeatureAll define feature name for all features + FeatureAll Feature = "*" + + // FeatureArangoDB define feature name for ArangoDB + FeatureArangoDB Feature = "ArangoDB" + + // FeatureArangoSearch define feature name for ArangoSearch + FeatureArangoSearch Feature = "ArangoSearch" + + // FeatureArangoML define feature name for ArangoML + FeatureArangoML Feature = "ArangoML" +) + +func (f Feature) In(features []Feature) bool { + for _, v := range features { + if v == f { + return true + } + } + return false +} + type License interface { // Validate validates the license scope. In case of: // - if feature is '*' - checks if: @@ -84,5 +107,6 @@ type License interface { // --- checks if subFeature or '*' is in the list of License Feature enabled SubFeatures Validate(feature Feature, subFeatures ...Feature) Status + // Refresh refreshes the license from the source (Secret) and verifies the signature Refresh(ctx context.Context) error } diff --git a/pkg/license/loader.go b/pkg/license/loader.go index 6ec7c8cd0..5b5373a18 100644 --- a/pkg/license/loader.go +++ b/pkg/license/loader.go @@ -23,6 +23,7 @@ package license import "context" type Loader interface { - // Refresh reloads license in specified manner, returns license, found, error + // Refresh reloads license in a specified manner. + // It returns license (base64 encoded), found, error Refresh(ctx context.Context) (string, bool, error) } diff --git a/pkg/license/loader_arangodeployment.go b/pkg/license/loader_arangodeployment.go index 7a598d667..6c01d5e6a 100644 --- a/pkg/license/loader_arangodeployment.go +++ b/pkg/license/loader_arangodeployment.go @@ -26,48 +26,34 @@ import ( "k8s.io/apimachinery/pkg/api/errors" meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/util/constants" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" - "github.com/arangodb/kube-arangodb/pkg/util/kclient" ) -func NewArengoDeploymentLicenseLoader(factory kclient.Factory, namespace, name string) Loader { +func NewArangoDeploymentLicenseLoader(client kubernetes.Interface, deployment *api.ArangoDeployment) Loader { return arangoDeploymentLicenseLoader{ - factory: factory, - namespace: namespace, - name: name, + client: client, + deployment: deployment, } } type arangoDeploymentLicenseLoader struct { - factory kclient.Factory + client kubernetes.Interface - namespace, name string + deployment *api.ArangoDeployment } func (a arangoDeploymentLicenseLoader) Refresh(ctx context.Context) (string, bool, error) { - client, ok := a.factory.Client() - if !ok { - return "", false, nil - } - - deployment, err := client.Arango().DatabaseV1().ArangoDeployments(a.namespace).Get(ctx, a.name, meta.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return "", false, nil - } - - return "", false, err - } - - spec := deployment.GetAcceptedSpec() + spec := a.deployment.GetAcceptedSpec() if !spec.License.HasSecretName() { return "", false, nil } - secret, err := client.Kubernetes().CoreV1().Secrets(deployment.GetNamespace()).Get(ctx, spec.License.GetSecretName(), meta.GetOptions{}) + secret, err := a.client.CoreV1().Secrets(a.deployment.GetNamespace()).Get(ctx, spec.License.GetSecretName(), meta.GetOptions{}) if err != nil { if errors.IsNotFound(err) { return "", false, nil diff --git a/pkg/license/loader_test.go b/pkg/license/loader_test.go new file mode 100644 index 000000000..88d6c9472 --- /dev/null +++ b/pkg/license/loader_test.go @@ -0,0 +1,35 @@ +// +// DISCLAIMER +// +// Copyright 2023 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany + +package license + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +type MockLoader struct { + mock.Mock +} + +func (m *MockLoader) Refresh(ctx context.Context) (string, bool, error) { + args := m.Called(ctx) + return args.String(0), args.Bool(1), args.Error(2) +} diff --git a/pkg/util/cert/signer.go b/pkg/util/cert/signer.go new file mode 100644 index 000000000..cd33d7928 --- /dev/null +++ b/pkg/util/cert/signer.go @@ -0,0 +1,71 @@ +// +// DISCLAIMER +// +// Copyright 2023 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package cert + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" +) + +type Signer struct { + privateKey *rsa.PrivateKey +} + +// NewSigner creates a new Signer with a generated private key. +func NewSigner() (*Signer, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + return &Signer{privateKey: privateKey}, nil +} + +// Sign signs the content with the private key and returns: +// base64 encoded signature, base64 encoded content and error. +func (s *Signer) Sign(content string) (string, string, error) { + hash := sha256.New() + hash.Write([]byte(content)) + signature, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, hash.Sum(nil)) + if err != nil { + return "", "", err + } + return base64.StdEncoding.EncodeToString(signature), base64.StdEncoding.EncodeToString([]byte(content)), nil +} + +// PublicKey returns the public key in PKIX format. +func (s *Signer) PublicKey() (string, error) { + publicKey := &s.privateKey.PublicKey + publicKeyDer, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", err + } + + publicKeyBlock := pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: publicKeyDer, + } + return string(pem.EncodeToMemory(&publicKeyBlock)), nil +}