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:
cuisongliu
2025-11-18 23:25:57 +08:00
committed by GitHub
parent cbeb965a02
commit f3eaedf92d
43 changed files with 1266 additions and 509 deletions
+8 -2
View File
@@ -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
-1
View File
@@ -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) || '' }}
-1
View File
@@ -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
+2 -2
View File
@@ -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
+9 -7
View File
@@ -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 {
+88 -45
View File
@@ -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
-14
View File
@@ -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 -5
View File
@@ -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
+6
View File
@@ -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
@@ -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")
+6 -2
View File
@@ -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
}
-1
View File
@@ -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
+30
View File
@@ -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)
}
}
@@ -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:
@@ -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: