mirror of
https://github.com/labring/sealos.git
synced 2026-10-29 17:25:58 +08:00
fix(license): update CRD version and add additional printer columns f… (#6212)
* fix(license): update CRD version and add additional printer columns for license status (#6207)
* feat(license): implement license controller with Helm chart and RBAC configuration
* fix(license): correct logic for extending default license for devbox users
* fix(license): update license validation to use context for improved handling
* fix(license): change EncryptionKey to encryptionKey and add getter function
* fix(license): extend default license expiration for devbox users to 50 years
(cherry picked from commit 2df0bdf854)
This commit is contained in:
@@ -54,7 +54,6 @@ env:
|
||||
# Common versions
|
||||
GO_VERSION: "1.24"
|
||||
DEFAULT_OWNER: "labring"
|
||||
CRYPTOKEY: ${{ secrets.CONTROLLER_BUILD_CRYPTOKEY }}
|
||||
LICENSE_KEY: ${{ secrets.LICENSE_KEY }}
|
||||
ALIYUN_REGISTRY: ${{ secrets.ALIYUN_REGISTRY }}
|
||||
ALIYUN_REPO_PREFIX: ${{ secrets.ALIYUN_REPO_PREFIX && secrets.ALIYUN_REPO_PREFIX || secrets.ALIYUN_USERNAME && format('{0}/{1}', secrets.ALIYUN_REGISTRY, secrets.ALIYUN_USERNAME) || '' }}
|
||||
@@ -304,7 +303,14 @@ jobs:
|
||||
run: |
|
||||
OLD_DOCKER_IMAGE_NAME="${OLD_DOCKER_REPO}:latest"
|
||||
NEW_DOCKER_IMAGE_NAME="${NEW_DOCKER_REPO}:${TAG_NAME}"
|
||||
sudo sed -i "s;${OLD_DOCKER_IMAGE_NAME};${NEW_DOCKER_IMAGE_NAME};" manifests/*
|
||||
if [[ -d manifests ]]; then
|
||||
echo "Found manifests"
|
||||
sudo sed -i "s;${OLD_DOCKER_IMAGE_NAME};${NEW_DOCKER_IMAGE_NAME};" manifests/*
|
||||
fi
|
||||
if [[ -f charts/${{ matrix.module.path }}/values.yaml ]]; then
|
||||
echo "Found charts charts/${{ matrix.module.path }}/values.yaml"
|
||||
sed -i "s;${OLD_DOCKER_IMAGE_NAME};${NEW_DOCKER_IMAGE_NAME};" charts/${{ matrix.module.path }}/values.yaml
|
||||
fi
|
||||
|
||||
- name: Build ${{ matrix.module.name }}-controller cluster image
|
||||
working-directory: controllers/${{ matrix.module.path }}/deploy
|
||||
|
||||
@@ -53,7 +53,6 @@ env:
|
||||
# Common versions
|
||||
GO_VERSION: "1.22"
|
||||
DEFAULT_OWNER: "labring"
|
||||
CRYPTOKEY: ${{ secrets.CONTROLLER_BUILD_CRYPTOKEY }}
|
||||
ALIYUN_REGISTRY: ${{ secrets.ALIYUN_REGISTRY }}
|
||||
ALIYUN_REPO_PREFIX: ${{ secrets.ALIYUN_REPO_PREFIX && secrets.ALIYUN_REPO_PREFIX || secrets.ALIYUN_USERNAME && format('{0}/{1}', secrets.ALIYUN_REGISTRY, secrets.ALIYUN_USERNAME) || '' }}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ CONTROLLER_PKG=github.com/labring/sealos/controllers/pkg
|
||||
CONTROLLER_LICENSE=github.com/labring/sealos/controllers/license/internal/controller
|
||||
build: fmt vet ## Build manager binary.
|
||||
LD_FLAGS="-s -w"; \
|
||||
[ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/preset-${GOARCH} cmd/preset/main.go && chmod +x bin/preset-${GOARCH} && cp bin/preset-${GOARCH} bin/manager
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ help: ## Display this help.
|
||||
.PHONY: manifests
|
||||
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
|
||||
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
|
||||
cp -r config/crd/bases/* deploy/charts/license/crds/
|
||||
|
||||
.PHONY: generate
|
||||
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
|
||||
@@ -69,8 +70,7 @@ CONTROLLER_PKG=github.com/labring/sealos/controllers/pkg
|
||||
CONTROLLER_LICENSE=github.com/labring/sealos/controllers/license/internal
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
LD_FLAGS="-s -w"; \
|
||||
[ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_PKG}/crypto.encryptionKey=${CRYPTOKEY} -X ${CONTROLLER_PKG}/database.cryptoKey=${CRYPTOKEY}"; \
|
||||
[ -n "$(LICENSE_KEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_LICENSE}/util/key.EncryptionKey=${LICENSE_KEY}"; \
|
||||
[ -n "$(LICENSE_KEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_LICENSE}/util/key.encryptionKey=${LICENSE_KEY}"; \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager cmd/manager/main.go
|
||||
|
||||
|
||||
|
||||
@@ -23,13 +23,11 @@ import (
|
||||
type LicenseType string
|
||||
|
||||
const (
|
||||
AccountLicenseType LicenseType = "Account"
|
||||
ClusterLicenseType LicenseType = "Cluster"
|
||||
)
|
||||
|
||||
// LicenseSpec defines the desired state of License
|
||||
type LicenseSpec struct {
|
||||
//+kubebuilder:validation:Enum=Account;Cluster
|
||||
Type LicenseType `json:"type,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
@@ -37,9 +35,12 @@ type LicenseSpec struct {
|
||||
type LicenseStatusPhase string
|
||||
|
||||
const (
|
||||
LicenseStatusPhasePending LicenseStatusPhase = "Pending"
|
||||
LicenseStatusPhaseFailed LicenseStatusPhase = "Failed"
|
||||
LicenseStatusPhaseActive LicenseStatusPhase = "Active"
|
||||
LicenseStatusPhasePending LicenseStatusPhase = "Pending"
|
||||
LicenseStatusPhaseFailed LicenseStatusPhase = "Failed"
|
||||
LicenseStatusPhaseActive LicenseStatusPhase = "Active"
|
||||
LicenseStatusPhaseExpired LicenseStatusPhase = "Expired"
|
||||
LicenseStatusPhaseInvalid LicenseStatusPhase = "Invalid"
|
||||
LicenseStatusPhaseMismatch LicenseStatusPhase = "Mismatch"
|
||||
)
|
||||
|
||||
type ValidationCode int
|
||||
@@ -54,10 +55,9 @@ const (
|
||||
|
||||
// LicenseStatus defines the observed state of License
|
||||
type LicenseStatus struct {
|
||||
//+kubebuilder:validation:Enum=Pending;Failed;Active
|
||||
//+kubebuilder:validation:Enum=Pending;Failed;Active;Expired;Invalid;Mismatch
|
||||
//+kubebuilder:default=Pending
|
||||
Phase LicenseStatusPhase `json:"phase,omitempty"`
|
||||
ValidationCode ValidationCode `json:"validationCode,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
ActivationTime metav1.Time `json:"activationTime,omitempty"`
|
||||
ExpirationTime metav1.Time `json:"expirationTime,omitempty"`
|
||||
@@ -65,6 +65,8 @@ type LicenseStatus struct {
|
||||
|
||||
//+kubebuilder:object:root=true
|
||||
//+kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
|
||||
// License is the Schema for the licenses API
|
||||
type License struct {
|
||||
|
||||
@@ -17,29 +17,22 @@ limitations under the License.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
database2 "github.com/labring/sealos/controllers/pkg/database"
|
||||
"github.com/labring/sealos/controllers/pkg/database/cockroach"
|
||||
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
|
||||
accountv1 "github.com/labring/sealos/controllers/account/api/v1"
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
"github.com/labring/sealos/controllers/license/internal/controller"
|
||||
utilid "github.com/labring/sealos/controllers/license/internal/util/clusterid"
|
||||
"github.com/labring/sealos/controllers/license/internal/util/database"
|
||||
//+kubebuilder:scaffold:imports
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -50,7 +43,6 @@ var (
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(accountv1.AddToScheme(scheme))
|
||||
utilruntime.Must(licensev1.AddToScheme(scheme))
|
||||
//+kubebuilder:scaffold:scheme
|
||||
}
|
||||
@@ -59,12 +51,33 @@ func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.StringVar(
|
||||
&metricsAddr,
|
||||
"metrics-bind-address",
|
||||
":8080",
|
||||
"The address the metric endpoint binds to.",
|
||||
)
|
||||
flag.StringVar(
|
||||
&probeAddr,
|
||||
"health-probe-bind-address",
|
||||
":8081",
|
||||
"The address the probe endpoint binds to.",
|
||||
)
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(
|
||||
&secureMetrics,
|
||||
"metrics-secure",
|
||||
true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.",
|
||||
)
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
@@ -73,11 +86,45 @@ func main() {
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
|
||||
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
},
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "90cbb0a1.sealos.io",
|
||||
@@ -97,34 +144,30 @@ func main() {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := database.New(context.Background(), os.Getenv("MONGO_URI"))
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to connect to database")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Disconnect(context.Background())
|
||||
|
||||
clusterID, err := utilid.GetClusterID(mgr.GetConfig())
|
||||
ctx := ctrl.SetupSignalHandler()
|
||||
clusterID, err := utilid.GetClusterID(ctx, mgr.GetConfig())
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to get cluster id")
|
||||
os.Exit(1)
|
||||
}
|
||||
setupLog.Info("cluster id", "id", clusterID)
|
||||
|
||||
accountDB, err := cockroach.NewCockRoach(os.Getenv(database2.GlobalCockroachURI), os.Getenv(database2.LocalCockroachURI))
|
||||
createTime, err := utilid.GetClusterCreateTime(ctx, mgr.GetConfig())
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to connect to cockroach")
|
||||
setupLog.Error(err, "unable to get cluster create time")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
err := accountDB.Close()
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to disconnect from cockroach")
|
||||
}
|
||||
}()
|
||||
setupLog.Info(
|
||||
"cluster id and create time",
|
||||
"id",
|
||||
clusterID,
|
||||
"createTime",
|
||||
createTime.String(),
|
||||
)
|
||||
|
||||
if err = (&controller.LicenseReconciler{ClusterID: clusterID}).SetupWithManager(mgr, accountDB); err != nil {
|
||||
reconciler := &controller.LicenseReconciler{
|
||||
ClusterID: clusterID,
|
||||
CreateTimestamp: *createTime,
|
||||
}
|
||||
if err = reconciler.SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "License")
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -140,7 +183,7 @@ func main() {
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
if err := mgr.Start(ctx); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
# Copyright © 2024 sealos.
|
||||
#
|
||||
# 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.
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
@@ -28,7 +14,14 @@ spec:
|
||||
singular: license
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: License is the Schema for the licenses API
|
||||
@@ -56,9 +49,6 @@ spec:
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
enum:
|
||||
- Account
|
||||
- Cluster
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
@@ -76,11 +66,12 @@ spec:
|
||||
- Pending
|
||||
- Failed
|
||||
- Active
|
||||
- Expired
|
||||
- Invalid
|
||||
- Mismatch
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
validationCode:
|
||||
type: integer
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
# Copyright © 2024 sealos.
|
||||
#
|
||||
# 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.
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
|
||||
@@ -3,8 +3,6 @@ FROM scratch
|
||||
USER 65532:65532
|
||||
|
||||
COPY registry registry
|
||||
COPY manifests manifests
|
||||
|
||||
ENV MONGO_URI="mongodb://mongo:27017/resources"
|
||||
|
||||
CMD ["kubectl apply -f manifests"]
|
||||
COPY charts charts
|
||||
COPY entrypoint.sh entrypoint.sh
|
||||
CMD ["bash entrypoint.sh"]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: license
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
name: licenses.license.sealos.io
|
||||
spec:
|
||||
group: license.sealos.io
|
||||
names:
|
||||
kind: License
|
||||
listKind: LicenseList
|
||||
plural: licenses
|
||||
singular: license
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: License is the Schema for the licenses API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: LicenseSpec defines the desired state of License
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: LicenseStatus defines the observed state of License
|
||||
properties:
|
||||
activationTime:
|
||||
format: date-time
|
||||
type: string
|
||||
expirationTime:
|
||||
format: date-time
|
||||
type: string
|
||||
phase:
|
||||
default: Pending
|
||||
enum:
|
||||
- Pending
|
||||
- Failed
|
||||
- Active
|
||||
- Expired
|
||||
- Invalid
|
||||
- Mismatch
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "license.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "license.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "license.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "license.labels" -}}
|
||||
helm.sh/chart: {{ include "license.chart" . }}
|
||||
{{ include "license.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "license.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "license.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "license.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "license.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,27 @@
|
||||
{{- if .Values.metrics.enabled }}
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Issuer
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "license.labels" . | nindent 4 }}
|
||||
name: selfsigned-issuer
|
||||
spec:
|
||||
selfSigned: {}
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "license.labels" . | nindent 4 }}
|
||||
name: metrics-certs
|
||||
spec:
|
||||
privateKey:
|
||||
rotationPolicy: Always
|
||||
dnsNames:
|
||||
- {{ include "license.fullname" . }}-metrics.{{.Release.Namespace}}.svc
|
||||
- {{ include "license.fullname" . }}-metrics.{{.Release.Namespace}}.svc.cluster.local
|
||||
issuerRef:
|
||||
kind: Issuer
|
||||
name: selfsigned-issuer
|
||||
secretName: {{.Values.metrics.secretName}}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,100 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: license-controller-clusterrole
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ "namespaces" ]
|
||||
resourceNames: [ "kube-system" ]
|
||||
verbs: [ "get" ]
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- license.sealos.io
|
||||
resources:
|
||||
- licenses
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- license.sealos.io
|
||||
resources:
|
||||
- licenses/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- license.sealos.io
|
||||
resources:
|
||||
- licenses/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- apiextensions.k8s.io
|
||||
resources:
|
||||
- customresourcedefinitions
|
||||
resourceNames:
|
||||
- devboxes.devbox.sealos.io
|
||||
verbs:
|
||||
- get
|
||||
- nonResourceURLs:
|
||||
- /metrics
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: license-controller-clusterrolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: license-controller-clusterrole
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "license.serviceAccountName" . }}
|
||||
namespace: {{.Release.Namespace}}
|
||||
---
|
||||
## ClusterRole for license frontend to get node info
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: license-manager-role
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ "namespaces" ]
|
||||
resourceNames: [ "kube-system" ]
|
||||
verbs: [ "get" ]
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- license.sealos.io
|
||||
resources:
|
||||
- licenses
|
||||
verbs:
|
||||
- "*"
|
||||
@@ -0,0 +1,91 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "license.fullname" . }}
|
||||
labels:
|
||||
{{- include "license.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "license.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "license.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "license.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
command:
|
||||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
- --health-probe-bind-address=:8081
|
||||
{{- if .Values.metrics.enabled }}
|
||||
- --metrics-secure=true
|
||||
- --metrics-bind-address=:8443
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image }}"
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
|
||||
volumeMounts:
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.metrics.enabled }}
|
||||
- name: metrics-certs
|
||||
mountPath: {{.Values.metrics.certPath}}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
|
||||
volumes:
|
||||
{{- if .Values.metrics.enabled }}
|
||||
- name: metrics-certs
|
||||
secret:
|
||||
secretName: {{.Values.metrics.secretName}}
|
||||
optional: false
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
- key: tls.crt
|
||||
path: tls.crt
|
||||
- key: tls.key
|
||||
path: tls.key
|
||||
{{- end }}
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: license-controller-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- '*'
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: license-controller-clusterrolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: license-controller-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "license.serviceAccountName" . }}
|
||||
namespace: {{.Release.Namespace}}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.metrics.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "license.fullname" . }}-metrics
|
||||
labels:
|
||||
{{- include "license.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 8443
|
||||
protocol: TCP
|
||||
name: https
|
||||
selector:
|
||||
{{- include "license.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,14 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "license.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "license.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
---
|
||||
@@ -0,0 +1,109 @@
|
||||
# Default values for license.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
image: ghcr.io/labring/sealos-license-controller:latest
|
||||
|
||||
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
#This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- "ALL"
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
port: 80
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8081
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
metrics:
|
||||
# Enable metrics endpoint
|
||||
enabled: false
|
||||
# Path to the metrics certificate
|
||||
certPath: /tmp/k8s-metrics-server/metrics-certs
|
||||
secretName: metrics-server-cert
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
HELM_OPTS=${HELM_OPTS:-""}
|
||||
kubectl delete --ignore-not-found -f uninstall
|
||||
kubectl delete clusterrole license-manager-role --ignore-not-found
|
||||
helm upgrade -i license -n license-system --create-namespace ./charts/license ${HELM_OPTS}
|
||||
helm show crds ./charts/license | kubectl apply -f - --server-side --force-conflicts
|
||||
+20
-20
@@ -29,8 +29,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.8.0
|
||||
creationTimestamp: null
|
||||
controller-gen.kubebuilder.io/version: v0.14.0
|
||||
name: licenses.license.sealos.io
|
||||
spec:
|
||||
group: license.sealos.io
|
||||
@@ -41,20 +40,32 @@ spec:
|
||||
singular: license
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: License is the Schema for the licenses API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
@@ -65,7 +76,6 @@ spec:
|
||||
type: string
|
||||
type:
|
||||
enum:
|
||||
- Account
|
||||
- Cluster
|
||||
type: string
|
||||
type: object
|
||||
@@ -95,12 +105,6 @@ spec:
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
@@ -163,7 +167,6 @@ rules:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: license-manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
@@ -381,9 +384,6 @@ spec:
|
||||
- --leader-elect
|
||||
command:
|
||||
- /manager
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: account-manager-env
|
||||
image: ghcr.io/labring/sealos-license-controller:latest
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -16,65 +16,45 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
claimsutil "github.com/labring/sealos/controllers/license/internal/util/claims"
|
||||
licenseutil "github.com/labring/sealos/controllers/license/internal/util/license"
|
||||
count "github.com/labring/sealos/controllers/pkg/account"
|
||||
database2 "github.com/labring/sealos/controllers/pkg/database"
|
||||
types2 "github.com/labring/sealos/controllers/pkg/types"
|
||||
"github.com/labring/sealos/controllers/pkg/utils/logger"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type LicenseActivator struct {
|
||||
client.Client
|
||||
accountDB database2.AccountV2
|
||||
}
|
||||
|
||||
func (l *LicenseActivator) Active(license *licensev1.License) error {
|
||||
// TODO mv to active function
|
||||
if license.Spec.Type == licensev1.AccountLicenseType {
|
||||
if err := l.Recharge(license); err != nil {
|
||||
return fmt.Errorf("recharge account failed: %w", err)
|
||||
func (r *LicenseActivator) updateStatus(
|
||||
ctx context.Context,
|
||||
nn types.NamespacedName,
|
||||
status *licensev1.LicenseStatus,
|
||||
) error {
|
||||
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
original := &licensev1.License{}
|
||||
if err := r.Get(ctx, nn, original); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
original.Status = *status
|
||||
return r.Client.Status().Update(ctx, original)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *LicenseActivator) Active(ctx context.Context, license *licensev1.License) error {
|
||||
exp, err := licenseutil.GetLicenseExpireTime(license)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
license.Status.ExpirationTime = metav1.NewTime(exp)
|
||||
license.Status.ActivationTime = metav1.NewTime(time.Now())
|
||||
license.Status.Phase = licensev1.LicenseStatusPhaseActive
|
||||
updateStatus := &license.Status
|
||||
updateStatus.Phase = licensev1.LicenseStatusPhaseActive
|
||||
updateStatus.Reason = "License activated successfully"
|
||||
updateStatus.ExpirationTime = metav1.NewTime(exp)
|
||||
updateStatus.ActivationTime = metav1.NewTime(time.Now())
|
||||
|
||||
if err := l.Status().Update(context.Background(), license); err != nil {
|
||||
return fmt.Errorf("update license status failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LicenseActivator) Recharge(license *licensev1.License) error {
|
||||
claims, err := licenseutil.GetClaims(license)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data = &claimsutil.AccountClaimData{}
|
||||
if err := claims.Data.SwitchToAccountData(data); err != nil {
|
||||
return err
|
||||
}
|
||||
owner := GetNameByNameSpace(license.Namespace)
|
||||
|
||||
logger.Info("recharge account", "crName", owner, "amount", data.Amount)
|
||||
|
||||
return l.accountDB.AddBalance(&types2.UserQueryOpts{Owner: owner}, data.Amount*count.CurrencyUnit)
|
||||
}
|
||||
|
||||
func GetNameByNameSpace(ns string) string {
|
||||
return strings.TrimPrefix(ns, "ns-")
|
||||
return r.updateStatus(ctx, client.ObjectKeyFromObject(license), updateStatus)
|
||||
}
|
||||
|
||||
@@ -18,20 +18,27 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
licenseutil "github.com/labring/sealos/controllers/license/internal/util/license"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
licenseutil "github.com/labring/sealos/controllers/license/internal/util/license"
|
||||
database2 "github.com/labring/sealos/controllers/pkg/database"
|
||||
)
|
||||
|
||||
// LicenseReconciler reconciles a License object
|
||||
@@ -39,15 +46,29 @@ type LicenseReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
Logger logr.Logger
|
||||
//finalizer *ctrlsdk.Finalizer
|
||||
// finalizer *ctrlsdk.Finalizer
|
||||
|
||||
ClusterID string
|
||||
ClusterID string
|
||||
CreateTimestamp metav1.Time
|
||||
|
||||
validator *LicenseValidator
|
||||
activator *LicenseActivator
|
||||
}
|
||||
|
||||
var requeueRes = ctrl.Result{RequeueAfter: time.Minute}
|
||||
var (
|
||||
requeueRes = ctrl.Result{RequeueAfter: time.Minute}
|
||||
longRequeueRes = ctrl.Result{RequeueAfter: 30 * time.Minute}
|
||||
immediateRequeue = ctrl.Result{Requeue: true}
|
||||
)
|
||||
|
||||
const (
|
||||
licenseFinalizer = "license.sealos.io/finalizer"
|
||||
licenseProtectionFinalizer = "license.sealos.io/default-license"
|
||||
defaultLicenseName = "default"
|
||||
defaultLicenseNamespace = "ns-admin"
|
||||
)
|
||||
|
||||
var defaultLicenseDuration = 30 * 24 * time.Hour
|
||||
|
||||
// +kubebuilder:rbac:groups=license.sealos.io,resources=licenses,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=license.sealos.io,resources=licenses/status,verbs=get;update;patch
|
||||
@@ -60,75 +81,115 @@ func (r *LicenseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
if err := r.Get(ctx, req.NamespacedName, license); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
if !license.DeletionTimestamp.IsZero() {
|
||||
|
||||
if license.DeletionTimestamp.IsZero() {
|
||||
// Ensure required finalizers exist; reconcile only after they are set.
|
||||
addedFinalizer := false
|
||||
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
latest := &licensev1.License{}
|
||||
if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
|
||||
return err
|
||||
}
|
||||
changed := controllerutil.AddFinalizer(latest, licenseFinalizer)
|
||||
if isDefaultLicense(latest) {
|
||||
changed = controllerutil.AddFinalizer(latest, licenseProtectionFinalizer) || changed
|
||||
}
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
addedFinalizer = true
|
||||
return r.Update(ctx, latest)
|
||||
}); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err)
|
||||
}
|
||||
if addedFinalizer {
|
||||
// Requeue immediately so the reconciler can proceed with the next step.
|
||||
return immediateRequeue, nil
|
||||
}
|
||||
} else {
|
||||
if controllerutil.ContainsFinalizer(license, licenseProtectionFinalizer) && isDefaultLicense(license) {
|
||||
r.Logger.Info("deletion blocked by protection finalizer", "license", req.NamespacedName)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
latest := &licensev1.License{}
|
||||
if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
|
||||
return err
|
||||
}
|
||||
if controllerutil.RemoveFinalizer(latest, licenseFinalizer) {
|
||||
return r.Update(ctx, latest)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %w", err)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if isDefaultLicense(license) {
|
||||
if err := r.ensureAndUpdateDefaultLicense(ctx, license); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return longRequeueRes, nil
|
||||
}
|
||||
return r.reconcile(ctx, license)
|
||||
}
|
||||
|
||||
func (r *LicenseReconciler) reconcile(ctx context.Context, license *licensev1.License) (ctrl.Result, error) {
|
||||
r.Logger.V(1).Info("reconcile for license", "license", license.Namespace+"/"+license.Name)
|
||||
// check if license is valid
|
||||
valid, err := r.validator.Validate(license)
|
||||
if err != nil {
|
||||
r.Logger.V(1).Error(err, "failed to validate license")
|
||||
func (r *LicenseReconciler) reconcile(
|
||||
ctx context.Context,
|
||||
license *licensev1.License,
|
||||
) (ctrl.Result, error) {
|
||||
nsName := fmt.Sprintf("%s/%s", license.Namespace, license.Name)
|
||||
r.Logger.V(1).Info("reconcile for license", "license", nsName)
|
||||
|
||||
if err := r.validator.Validate(ctx, license); err != nil {
|
||||
// Extract validation code if it's a ValidationError
|
||||
var reason string
|
||||
|
||||
var validationErr licenseutil.ValidationError
|
||||
var phase licensev1.LicenseStatusPhase
|
||||
if errors.As(err, &validationErr) {
|
||||
reason = validationErr.Message
|
||||
switch validationErr.Code {
|
||||
case licensev1.ValidationExpired:
|
||||
phase = licensev1.LicenseStatusPhaseExpired
|
||||
case licensev1.ValidationClusterInfoMismatch:
|
||||
phase = licensev1.LicenseStatusPhaseInvalid
|
||||
case licensev1.ValidationClusterIDMismatch:
|
||||
phase = licensev1.LicenseStatusPhaseMismatch
|
||||
default:
|
||||
phase = licensev1.LicenseStatusPhaseFailed
|
||||
}
|
||||
} else {
|
||||
// Handle non-ValidationError
|
||||
reason = fmt.Sprintf("license validation failed: %v", err)
|
||||
phase = licensev1.LicenseStatusPhaseFailed
|
||||
}
|
||||
updateStatus := &license.Status
|
||||
updateStatus.Phase = phase
|
||||
updateStatus.Reason = reason
|
||||
updateStatus.ActivationTime = license.Status.ActivationTime
|
||||
updateStatus.ExpirationTime = license.Status.ExpirationTime
|
||||
if updateErr := r.updateStatus(ctx, client.ObjectKeyFromObject(license), updateStatus); updateErr != nil {
|
||||
r.Logger.V(1).
|
||||
Error(updateErr, "failed to update license status after validation failure", "license", nsName)
|
||||
return requeueRes, updateErr
|
||||
}
|
||||
r.Logger.V(1).Error(err, "failed to validate license", "license", nsName)
|
||||
return requeueRes, err
|
||||
}
|
||||
|
||||
claims, err := licenseutil.GetClaims(license)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
switch valid {
|
||||
case licensev1.ValidationClusterIDMismatch:
|
||||
license.Status.Phase = licensev1.LicenseStatusPhaseFailed
|
||||
license.Status.ValidationCode = valid
|
||||
license.Status.Reason = fmt.Sprintf("cluster id mismatch, license cluster id: %s, cluster id: %s", claims.ClusterID, r.ClusterID)
|
||||
r.Logger.V(1).Info("cluster id mismatch", "license", license.Namespace+"/"+license.Name, "cluster id", r.ClusterID, "license cluster id", claims.ClusterID)
|
||||
_ = r.Status().Update(ctx, license)
|
||||
return requeueRes, nil
|
||||
case licensev1.ValidationClusterInfoMismatch:
|
||||
license.Status.Phase = licensev1.LicenseStatusPhaseFailed
|
||||
license.Status.ValidationCode = valid
|
||||
license.Status.Reason = fmt.Sprintf("cluster info mismatch, license cluster info: %v", claims.Data)
|
||||
r.Logger.V(1).Info("cluster info mismatch", "license", license.Namespace+"/"+license.Name, "cluster info", claims.Data)
|
||||
_ = r.Status().Update(ctx, license)
|
||||
return requeueRes, nil
|
||||
case licensev1.ValidationExpired:
|
||||
license.Status.Phase = licensev1.LicenseStatusPhaseFailed
|
||||
license.Status.ValidationCode = valid
|
||||
license.Status.Reason = "license is expired"
|
||||
r.Logger.V(1).Info("license is invalid", "license", license.Namespace+"/"+license.Name, "reason", valid)
|
||||
_ = r.Status().Update(ctx, license)
|
||||
return requeueRes, nil
|
||||
case licensev1.ValidationError:
|
||||
license.Status.Phase = licensev1.LicenseStatusPhaseFailed
|
||||
license.Status.ValidationCode = valid
|
||||
license.Status.Reason = "failed to validate license"
|
||||
r.Logger.V(1).Info("failed to validate license", "license", license.Namespace+"/"+license.Name)
|
||||
_ = r.Status().Update(ctx, license)
|
||||
return requeueRes, nil
|
||||
case licensev1.ValidationSuccess:
|
||||
// do nothing
|
||||
default:
|
||||
r.Logger.V(1).Info("unknown validation code", "code", valid)
|
||||
}
|
||||
|
||||
if license.Spec.Type == licensev1.AccountLicenseType && license.Status.Phase == licensev1.LicenseStatusPhaseActive {
|
||||
r.Logger.V(1).Info("license is active, skip reconcile", "license", license.Namespace+"/"+license.Name)
|
||||
return requeueRes, nil
|
||||
}
|
||||
|
||||
if err := r.activator.Active(license); err != nil {
|
||||
r.Logger.V(1).Error(err, "failed to active license")
|
||||
if err := r.activator.Active(ctx, license); err != nil {
|
||||
// Update license status to failed when activation fails
|
||||
r.Logger.V(1).Error(err, "failed to activate license", "license", nsName)
|
||||
return requeueRes, err
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: time.Minute * 30}, nil
|
||||
r.Logger.V(1).
|
||||
Info("license activated successfully", "license", nsName, "phase", license.Status.Phase)
|
||||
return longRequeueRes, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *LicenseReconciler) SetupWithManager(mgr ctrl.Manager, accountDB database2.AccountV2) error {
|
||||
func (r *LicenseReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
r.Logger = mgr.GetLogger().WithName("controller").WithName("License")
|
||||
r.Client = mgr.GetClient()
|
||||
|
||||
@@ -139,12 +200,149 @@ func (r *LicenseReconciler) SetupWithManager(mgr ctrl.Manager, accountDB databas
|
||||
}
|
||||
|
||||
r.activator = &LicenseActivator{
|
||||
Client: r.Client,
|
||||
accountDB: accountDB,
|
||||
Client: r.Client,
|
||||
}
|
||||
|
||||
if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
if err := r.ensureAndUpdateDefaultLicense(ctx, nil); err != nil {
|
||||
r.Logger.Error(err, "periodic default license ensure failed")
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if err := r.ensureAndUpdateDefaultLicense(ctx, nil); err != nil {
|
||||
r.Logger.Error(err, "periodic default license ensure failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
// reconcile on generation change
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
WithOptions(controller.Options{MaxConcurrentReconciles: 10}).
|
||||
For(&licensev1.License{}, builder.WithPredicates(predicate.And(predicate.GenerationChangedPredicate{}))).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func isDefaultLicense(license *licensev1.License) bool {
|
||||
return license.Name == defaultLicenseName && license.Namespace == defaultLicenseNamespace
|
||||
}
|
||||
|
||||
func (r *LicenseReconciler) ensureAndUpdateDefaultLicense(
|
||||
ctx context.Context,
|
||||
cached *licensev1.License,
|
||||
) error {
|
||||
ns := defaultLicenseNamespace
|
||||
namespacedName := types.NamespacedName{
|
||||
Name: defaultLicenseName,
|
||||
Namespace: ns,
|
||||
}
|
||||
|
||||
license := cached
|
||||
var err error
|
||||
if license == nil {
|
||||
license = &licensev1.License{}
|
||||
err = r.Get(ctx, namespacedName, license)
|
||||
}
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
// ensure finalizers
|
||||
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
latest := &licensev1.License{}
|
||||
if err := r.Get(ctx, namespacedName, latest); err != nil {
|
||||
return err
|
||||
}
|
||||
changed := controllerutil.AddFinalizer(latest, licenseFinalizer)
|
||||
if controllerutil.AddFinalizer(latest, licenseProtectionFinalizer) {
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
return r.Update(ctx, latest)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case apierrors.IsNotFound(err):
|
||||
license = &licensev1.License{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: defaultLicenseName,
|
||||
Namespace: ns,
|
||||
Finalizers: []string{
|
||||
licenseFinalizer,
|
||||
licenseProtectionFinalizer,
|
||||
},
|
||||
},
|
||||
Spec: licensev1.LicenseSpec{
|
||||
Type: licensev1.ClusterLicenseType,
|
||||
},
|
||||
}
|
||||
if err := r.Create(ctx, license); err != nil {
|
||||
return fmt.Errorf("failed to create default license: %w", err)
|
||||
}
|
||||
r.Logger.Info(
|
||||
"default license created",
|
||||
"license",
|
||||
fmt.Sprintf("%s/%s", ns, defaultLicenseName),
|
||||
)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
expiration := r.CreateTimestamp.Add(defaultLicenseDuration)
|
||||
devboxInstalled, err := r.isDevBoxInstalled(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check devbox installation: %w", err)
|
||||
}
|
||||
if !devboxInstalled {
|
||||
// extend default license for devbox users
|
||||
expiration = r.CreateTimestamp.Add(50 * 365 * 24 * time.Hour)
|
||||
}
|
||||
updateStatus := &license.Status
|
||||
updateStatus.ActivationTime = r.CreateTimestamp
|
||||
updateStatus.ExpirationTime = metav1.NewTime(expiration)
|
||||
if time.Now().After(expiration) {
|
||||
updateStatus.Phase = licensev1.LicenseStatusPhaseExpired
|
||||
updateStatus.Reason = "default license expired"
|
||||
} else {
|
||||
updateStatus.Phase = licensev1.LicenseStatusPhaseActive
|
||||
updateStatus.Reason = "default license active"
|
||||
}
|
||||
return r.updateStatus(ctx, client.ObjectKeyFromObject(license), updateStatus)
|
||||
}
|
||||
|
||||
func (r *LicenseReconciler) updateStatus(
|
||||
ctx context.Context,
|
||||
nn types.NamespacedName,
|
||||
status *licensev1.LicenseStatus,
|
||||
) error {
|
||||
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
original := &licensev1.License{}
|
||||
if err := r.Get(ctx, nn, original); err != nil {
|
||||
return err
|
||||
}
|
||||
original.Status = *status
|
||||
return r.Client.Status().Update(ctx, original)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *LicenseReconciler) isDevBoxInstalled(ctx context.Context) (bool, error) {
|
||||
crd := &unstructured.Unstructured{}
|
||||
crd.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Kind: "CustomResourceDefinition",
|
||||
})
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: "devboxes.devbox.sealos.io"}, crd); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -16,17 +16,16 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
"github.com/labring/sealos/controllers/license/internal/util/claims"
|
||||
"github.com/labring/sealos/controllers/license/internal/util/cluster"
|
||||
licenseutil "github.com/labring/sealos/controllers/license/internal/util/license"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type LicenseValidator struct {
|
||||
@@ -35,10 +34,10 @@ type LicenseValidator struct {
|
||||
ClusterID string
|
||||
}
|
||||
|
||||
func (v *LicenseValidator) Validate(license *licensev1.License) (licensev1.ValidationCode, error) {
|
||||
func (v *LicenseValidator) Validate(ctx context.Context, license *licensev1.License) error {
|
||||
nodeList := &v1.NodeList{}
|
||||
if err := v.Client.List(context.Background(), nodeList); err != nil {
|
||||
return licensev1.ValidationError, err
|
||||
if err := v.List(ctx, nodeList); err != nil {
|
||||
return fmt.Errorf("failed to list cluster nodes: %w", err)
|
||||
}
|
||||
nodeCount := len(nodeList.Items)
|
||||
totalCPU := resource.NewQuantity(0, resource.DecimalSI)
|
||||
@@ -58,6 +57,12 @@ func (v *LicenseValidator) Validate(license *licensev1.License) (licensev1.Valid
|
||||
TotalMemory: int(totalMemory.Value() / (1024 * 1024 * 1024)),
|
||||
},
|
||||
}
|
||||
v.Logger.Info("Validating license", "cluster info", clusterInfo, "license token", license.Spec.Token)
|
||||
v.Logger.Info(
|
||||
"Validating license",
|
||||
"cluster info",
|
||||
clusterInfo,
|
||||
"license token",
|
||||
license.Spec.Token,
|
||||
)
|
||||
return licenseutil.IsLicenseValid(license, clusterInfo, v.ClusterID)
|
||||
}
|
||||
|
||||
@@ -20,26 +20,25 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
var (
|
||||
cfg *rest.Config
|
||||
k8sClient client.Client
|
||||
testEnv *envtest.Environment
|
||||
)
|
||||
|
||||
func TestControllers(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
@@ -70,7 +69,6 @@ var _ = BeforeSuite(func() {
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient).NotTo(BeNil())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
|
||||
@@ -16,9 +16,8 @@ package claims
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
v1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// Claims is the data structure of license claims
|
||||
@@ -30,20 +29,12 @@ type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type ClaimData map[string]interface{}
|
||||
|
||||
func (c *ClaimData) SwitchToAccountData(data *AccountClaimData) error {
|
||||
return mapstructure.Decode(c, data)
|
||||
}
|
||||
type ClaimData map[string]any
|
||||
|
||||
func (c *ClaimData) SwitchToClusterData(data *ClusterClaimData) error {
|
||||
return mapstructure.Decode(c, data)
|
||||
}
|
||||
|
||||
type AccountClaimData struct {
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
|
||||
// ClusterClaimData is the data structure of cluster license
|
||||
// example:
|
||||
// {
|
||||
@@ -53,15 +44,16 @@ type AccountClaimData struct {
|
||||
// }
|
||||
|
||||
type ClusterClaimData struct {
|
||||
NodeCount int `json:"nodeCount"`
|
||||
TotalCPU int `json:"totalCPU"` // in core
|
||||
TotalMemory int `json:"totalMemory"` // in GB
|
||||
NodeCount int `json:"nodeCount" mapstructure:"nodeCount"`
|
||||
TotalCPU int `json:"totalCPU" mapstructure:"totalCPU"` // in core
|
||||
TotalMemory int `json:"totalMemory" mapstructure:"totalMemory"` // in GB
|
||||
}
|
||||
|
||||
// Compare compares the claims with the data
|
||||
// return true if the claims is equal or lager to the data
|
||||
func (c *ClusterClaimData) Compare(data *ClusterClaimData) bool {
|
||||
if c.NodeCount > data.NodeCount || c.TotalCPU > data.TotalCPU || c.TotalMemory > data.TotalMemory {
|
||||
if c.NodeCount > data.NodeCount || c.TotalCPU > data.TotalCPU ||
|
||||
c.TotalMemory > data.TotalMemory {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -15,41 +15,9 @@
|
||||
package claims
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClaimData_SwitchToAccountData(t *testing.T) {
|
||||
type args struct {
|
||||
data *AccountClaimData
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
c ClaimData
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
c: ClaimData{
|
||||
"amount": 100,
|
||||
},
|
||||
args: args{
|
||||
data: &AccountClaimData{},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.c.SwitchToAccountData(tt.args.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("SwitchToAccountData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
fmt.Printf("%v", tt.args.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimData_SwitchToClusterData(t *testing.T) {
|
||||
type args struct {
|
||||
data *ClusterClaimData
|
||||
|
||||
@@ -31,5 +31,5 @@ func (i *Info) CompareWithClaimData(data *claims.ClaimData) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return i.ClusterClaimData.Compare(cdata)
|
||||
return i.Compare(cdata)
|
||||
}
|
||||
|
||||
@@ -16,16 +16,16 @@ package clusterid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func GetClusterID(config *rest.Config) (string, error) {
|
||||
func GetClusterID(ctx context.Context, config *rest.Config) (string, error) {
|
||||
ns := &corev1.Namespace{}
|
||||
ctx := context.Background()
|
||||
c, err := client.New(config, client.Options{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -36,7 +36,24 @@ func GetClusterID(config *rest.Config) (string, error) {
|
||||
}
|
||||
res := string(ns.UID)
|
||||
if res == "" || len(res) < 8 {
|
||||
return "", fmt.Errorf("failed to get cluster id")
|
||||
return "", errors.New("failed to get cluster id")
|
||||
}
|
||||
return res[0:8], nil
|
||||
}
|
||||
|
||||
func GetClusterCreateTime(ctx context.Context, config *rest.Config) (*metav1.Time, error) {
|
||||
ns := &corev1.Namespace{}
|
||||
c, err := client.New(config, client.Options{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.Get(ctx, client.ObjectKey{Name: "kube-system"}, ns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := ns.GetCreationTimestamp()
|
||||
if res.IsZero() {
|
||||
return nil, errors.New("failed to get create time")
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright © 2023 sealos.
|
||||
//
|
||||
// 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/labring/sealos/controllers/license/internal/util/meta"
|
||||
"github.com/labring/sealos/controllers/pkg/utils/logger"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
mongoOptions "go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type DataBase struct {
|
||||
URI string
|
||||
Client *mongo.Client
|
||||
|
||||
licenseCollection *mongo.Collection
|
||||
clusterIDCollection *mongo.Collection
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultLicenseDataBase = "sealos-license"
|
||||
DefaultLicenseCollection = "license"
|
||||
DefaultClusterIDCollection = "cluster-id"
|
||||
)
|
||||
|
||||
func New(ctx context.Context, uri string) (*DataBase, error) {
|
||||
client, err := mongo.Connect(ctx, mongoOptions.Client().ApplyURI(uri))
|
||||
if err != nil {
|
||||
return &DataBase{}, err
|
||||
}
|
||||
if err := client.Ping(ctx, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DataBase{
|
||||
URI: uri,
|
||||
Client: client,
|
||||
licenseCollection: client.Database(DefaultLicenseDataBase).Collection(DefaultLicenseCollection),
|
||||
clusterIDCollection: client.Database(DefaultLicenseDataBase).Collection(DefaultClusterIDCollection),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *DataBase) StoreLicenseMeta(ctx context.Context, meta *meta.Meta) error {
|
||||
_, err := db.licenseCollection.InsertOne(ctx, meta)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DataBase) GetLicenseMeta(ctx context.Context, token string) (*meta.Meta, error) {
|
||||
filter := bson.M{"token": token}
|
||||
lic := &meta.Meta{}
|
||||
err := db.licenseCollection.FindOne(ctx, filter).Decode(lic)
|
||||
return lic, err
|
||||
}
|
||||
|
||||
func (db *DataBase) GetClusterID(ctx context.Context) (string, error) {
|
||||
var result struct {
|
||||
ClusterID string `bson:"cluster-id"`
|
||||
}
|
||||
err := db.licenseCollection.FindOne(ctx, bson.M{}).Decode(&result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.ClusterID, nil
|
||||
}
|
||||
|
||||
func (db *DataBase) StoreClusterID(ctx context.Context, clusterID string) error {
|
||||
document := bson.M{"cluster-id": clusterID}
|
||||
_, err := db.licenseCollection.InsertOne(ctx, document)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DataBase) Disconnect(ctx context.Context) {
|
||||
err := db.Client.Disconnect(ctx)
|
||||
if err != nil {
|
||||
logger.Error(err, "disconnect from database failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright © 2023 sealos.
|
||||
//
|
||||
// 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 database
|
||||
|
||||
// TODO Add tests
|
||||
@@ -15,12 +15,7 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var ErrLicenseInvalid = fmt.Errorf("the license provided appears to be invalid")
|
||||
var ErrLicenseTypeNotMatch = fmt.Errorf("the license type provided appears to be invalid")
|
||||
var ErrClaimsConvent = fmt.Errorf("the claims data provided appears to be invalid")
|
||||
var ErrClusterIDNotMatch = fmt.Errorf("the cluster id provided appears to be invalid")
|
||||
var ErrClusterLicenseNotMatch = fmt.Errorf("the cluster license provided appears to be invalid")
|
||||
var ErrLicenseExpired = fmt.Errorf("the license provided appears to be expired")
|
||||
var ErrClaimsConvent = errors.New("the claims data provided appears to be invalid")
|
||||
|
||||
@@ -14,5 +14,9 @@
|
||||
|
||||
package key
|
||||
|
||||
// EncryptionKey is the key used to decode license token, base64 encoded
|
||||
var EncryptionKey = "ZHd6b2Nyc3NrcXdwZm5lb2twZmV5dGNxa2JnZ3Znem8="
|
||||
// encryptionKey is the key used to decode license token, base64 encoded
|
||||
var encryptionKey = "ZHd6b2Nyc3NrcXdwZm5lb2twZmV5dGNxa2JnZ3Znem8="
|
||||
|
||||
func GetEncryptionKey() string {
|
||||
return encryptionKey
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ package license
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
utilclaims "github.com/labring/sealos/controllers/license/internal/util/claims"
|
||||
"github.com/labring/sealos/controllers/license/internal/util/cluster"
|
||||
@@ -28,14 +28,32 @@ import (
|
||||
"github.com/labring/sealos/controllers/pkg/crypto"
|
||||
)
|
||||
|
||||
// ValidationError represents a license validation error with structured information
|
||||
type ValidationError struct {
|
||||
Code licensev1.ValidationCode
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewValidationError creates a new ValidationError with the given code and message
|
||||
func NewValidationError(code licensev1.ValidationCode, message string) error {
|
||||
return ValidationError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseLicenseToken(license *licensev1.License) (*jwt.Token, error) {
|
||||
token, err := jwt.ParseWithClaims(license.Spec.Token, &utilclaims.Claims{},
|
||||
func(_ *jwt.Token) (interface{}, error) {
|
||||
decodeKey, err := base64.StdEncoding.DecodeString(key.EncryptionKey)
|
||||
func(_ *jwt.Token) (any, error) {
|
||||
decodeKey, err := base64.StdEncoding.DecodeString(key.GetEncryptionKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicKey, err := crypto.ParseRSAPublicKeyFromPEM(string(decodeKey))
|
||||
publicKey, err := crypto.ParseRSAPublicKeyFromAnyPEM(string(decodeKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,29 +77,67 @@ func GetClaims(license *licensev1.License) (*utilclaims.Claims, error) {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func IsLicenseValid(license *licensev1.License, clusterInfo *cluster.Info, clusterID string) (licensev1.ValidationCode, error) {
|
||||
func IsLicenseValid(
|
||||
license *licensev1.License,
|
||||
clusterInfo *cluster.Info,
|
||||
clusterID string,
|
||||
) error {
|
||||
token, err := ParseLicenseToken(license)
|
||||
if err != nil {
|
||||
return licensev1.ValidationError, err
|
||||
return NewValidationError(
|
||||
licensev1.ValidationError,
|
||||
fmt.Sprintf("failed to parse license token: %v", err),
|
||||
)
|
||||
}
|
||||
if !token.Valid {
|
||||
return licensev1.ValidationExpired, nil
|
||||
// Get the expiration time from claims to provide more detailed error message
|
||||
claims, ok := token.Claims.(*utilclaims.Claims)
|
||||
if ok && claims.ExpiresAt != nil {
|
||||
return NewValidationError(
|
||||
licensev1.ValidationExpired,
|
||||
"license has expired on "+claims.ExpiresAt.Format(time.DateTime),
|
||||
)
|
||||
}
|
||||
return NewValidationError(
|
||||
licensev1.ValidationExpired,
|
||||
"license has expired and is no longer valid",
|
||||
)
|
||||
}
|
||||
|
||||
claims, err := GetClaims(license)
|
||||
if err != nil {
|
||||
return licensev1.ValidationError, err
|
||||
return NewValidationError(
|
||||
licensev1.ValidationError,
|
||||
fmt.Sprintf("failed to get license claims: %v", err),
|
||||
)
|
||||
}
|
||||
|
||||
// if clusterID is empty, it means this license is a super license.
|
||||
if claims.ClusterID != "" && claims.ClusterID != clusterID {
|
||||
return licensev1.ValidationClusterIDMismatch, nil
|
||||
return NewValidationError(
|
||||
licensev1.ValidationClusterIDMismatch,
|
||||
fmt.Sprintf(
|
||||
"license cluster ID mismatch: license cluster ID is '%s' but current cluster ID is '%s'",
|
||||
claims.ClusterID,
|
||||
clusterID,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if claims.Type == licensev1.ClusterLicenseType {
|
||||
if !clusterInfo.CompareWithClaimData(&claims.Data) {
|
||||
return licensev1.ValidationClusterInfoMismatch, nil
|
||||
return NewValidationError(
|
||||
licensev1.ValidationClusterInfoMismatch,
|
||||
fmt.Sprintf(
|
||||
"license cluster constraints not met: license requires %v but current cluster does not satisfy these constraints",
|
||||
claims.Data,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
return licensev1.ValidationSuccess, nil
|
||||
|
||||
// License is valid
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetLicenseExpireTime(license *licensev1.License) (time.Time, error) {
|
||||
|
||||
@@ -15,15 +15,53 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
utilclaims "github.com/labring/sealos/controllers/license/internal/util/claims"
|
||||
"github.com/labring/sealos/controllers/license/internal/util/cluster"
|
||||
"github.com/labring/sealos/controllers/license/internal/util/key"
|
||||
)
|
||||
|
||||
func newTestLicense(t *testing.T) (*licensev1.License, *utilclaims.Claims) {
|
||||
t.Helper()
|
||||
decodeKey, err := base64.StdEncoding.DecodeString(key.GetEncryptionKey())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode encryption key: %v", err)
|
||||
}
|
||||
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(decodeKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse private key: %v", err)
|
||||
}
|
||||
claimData := utilclaims.ClaimData{
|
||||
"nodeCount": 3,
|
||||
"totalCPU": 16,
|
||||
"totalMemory": 32,
|
||||
}
|
||||
claims := &utilclaims.Claims{
|
||||
Type: licensev1.ClusterLicenseType,
|
||||
ClusterID: "test-cluster",
|
||||
Data: claimData,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
signed, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to sign test token: %v", err)
|
||||
}
|
||||
return &licensev1.License{
|
||||
Spec: licensev1.LicenseSpec{
|
||||
Token: signed,
|
||||
Type: claims.Type,
|
||||
},
|
||||
}, claims
|
||||
}
|
||||
|
||||
func TestIsLicenseValid(t *testing.T) {
|
||||
type args struct {
|
||||
license *licensev1.License
|
||||
@@ -35,34 +73,36 @@ func TestIsLicenseValid(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
args: args{
|
||||
license: &licensev1.License{
|
||||
Spec: licensev1.LicenseSpec{
|
||||
Token: "",
|
||||
Type: licensev1.ClusterLicenseType,
|
||||
name: "valid license",
|
||||
args: func() args {
|
||||
license, claims := newTestLicense(t)
|
||||
clusterData := &utilclaims.ClusterClaimData{}
|
||||
if err := claims.Data.SwitchToClusterData(clusterData); err != nil {
|
||||
t.Fatalf("failed to convert cluster data: %v", err)
|
||||
}
|
||||
return args{
|
||||
license: license,
|
||||
data: &cluster.Info{
|
||||
ClusterID: claims.ClusterID,
|
||||
ClusterClaimData: *clusterData,
|
||||
},
|
||||
},
|
||||
data: &cluster.Info{
|
||||
ClusterID: "",
|
||||
ClusterClaimData: utilclaims.ClusterClaimData{
|
||||
NodeCount: 3,
|
||||
TotalCPU: 16,
|
||||
TotalMemory: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := IsLicenseValid(tt.args.license, tt.args.data, "")
|
||||
err := IsLicenseValid(tt.args.license, tt.args.data, tt.args.data.ClusterID)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("IsLicenseValid() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
t.Log(got)
|
||||
if err != nil {
|
||||
// If there's an error, the test is expecting it, so we're done
|
||||
return
|
||||
}
|
||||
// If no error, validation succeeded, which is what we want
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -78,25 +118,27 @@ func TestParseLicenseToken(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
args: args{
|
||||
license: &licensev1.License{
|
||||
Spec: licensev1.LicenseSpec{
|
||||
Token: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
name: "parse valid token",
|
||||
args: func() args {
|
||||
license, _ := newTestLicense(t)
|
||||
return args{license: license}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseLicenseToken(tt.args.license)
|
||||
t.Log(got.Claims)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseLicenseToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got == nil || !got.Valid {
|
||||
t.Fatalf("ParseLicenseToken() returned invalid token: %+v", got)
|
||||
}
|
||||
if _, ok := got.Claims.(*utilclaims.Claims); !ok {
|
||||
t.Fatalf("ParseLicenseToken() claims type mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -105,6 +147,7 @@ func TestGetClaims(t *testing.T) {
|
||||
type args struct {
|
||||
license *licensev1.License
|
||||
}
|
||||
license, expectedClaims := newTestLicense(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
@@ -112,14 +155,9 @@ func TestGetClaims(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
args: args{
|
||||
license: &licensev1.License{
|
||||
Spec: licensev1.LicenseSpec{
|
||||
Token: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
name: "get claims from valid token",
|
||||
args: args{license: license},
|
||||
want: expectedClaims,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -129,7 +167,9 @@ func TestGetClaims(t *testing.T) {
|
||||
t.Errorf("GetClaims() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
t.Log(got)
|
||||
if got.Type != tt.want.Type || got.ClusterID != tt.want.ClusterID {
|
||||
t.Errorf("GetClaims() got = %+v, want = %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright © 2023 sealos.
|
||||
//
|
||||
// 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 meta
|
||||
|
||||
import (
|
||||
licensev1 "github.com/labring/sealos/controllers/license/api/v1"
|
||||
claimsutil "github.com/labring/sealos/controllers/license/internal/util/claims"
|
||||
licenseutil "github.com/labring/sealos/controllers/license/internal/util/license"
|
||||
)
|
||||
|
||||
// Meta is the license metadata, which will be stored in database
|
||||
|
||||
type Meta struct {
|
||||
Token string `bson:"token"`
|
||||
ActivationTime string `bson:"activationTime"`
|
||||
Claims claimsutil.Claims `bson:"claims"`
|
||||
}
|
||||
|
||||
func New(license *licensev1.License) (*Meta, error) {
|
||||
claims, err := licenseutil.GetClaims(license)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Meta{
|
||||
Token: license.Spec.Token,
|
||||
ActivationTime: license.Status.ActivationTime.Format("2006-01-02 15:04:05"),
|
||||
Claims: *claims,
|
||||
}, nil
|
||||
}
|
||||
@@ -64,7 +64,6 @@ test: manifests generate fmt vet envtest ## Run tests.
|
||||
.PHONY: build
|
||||
build: ## Build manager binary.
|
||||
LD_FLAGS="-s -w"; \
|
||||
[ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go
|
||||
|
||||
.PHONY: run
|
||||
|
||||
@@ -132,3 +132,33 @@ func ParseRSAPublicKeyFromPEM(keyPEM string) (*rsa.PublicKey, error) {
|
||||
}
|
||||
return rsaPub, nil
|
||||
}
|
||||
|
||||
// ParseRSAPublicKeyFromAnyPEM tries to parse a public key; if the PEM contains a private key it derives the public part.
|
||||
func ParseRSAPublicKeyFromAnyPEM(keyPEM string) (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode([]byte(keyPEM))
|
||||
if block == nil {
|
||||
return nil, errors.New("failed to parse PEM block")
|
||||
}
|
||||
switch block.Type {
|
||||
case "PUBLIC KEY":
|
||||
return ParseRSAPublicKeyFromPEM(keyPEM)
|
||||
case "RSA PRIVATE KEY":
|
||||
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &priv.PublicKey, nil
|
||||
case "PRIVATE KEY":
|
||||
privAny, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priv, ok := privAny.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("private key is not RSA")
|
||||
}
|
||||
return &priv.PublicKey, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type %q", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -1,3 +1,17 @@
|
||||
# Copyright © 2025 sealos.
|
||||
#
|
||||
# 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.
|
||||
|
||||
name: ❌ Close Pull Requests
|
||||
|
||||
on:
|
||||
|
||||
+14
@@ -1,3 +1,17 @@
|
||||
# Copyright © 2025 sealos.
|
||||
#
|
||||
# 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.
|
||||
|
||||
name: ❌ Close Pull Requests
|
||||
|
||||
on:
|
||||
|
||||
Reference in New Issue
Block a user