Skip to content

Commit

Permalink
Add Offline licenses related apis (#298)
Browse files Browse the repository at this point in the history
Signed-off-by: Rokibul Hasan <[email protected]>
Signed-off-by: Arnob kumar saha <[email protected]>
Signed-off-by: Tamal Saha <[email protected]>
Co-authored-by: Arnob kumar saha <[email protected]>
Co-authored-by: Tamal Saha <[email protected]>
  • Loading branch information
3 people authored May 15, 2024
1 parent 8cf2924 commit 01a6d42
Show file tree
Hide file tree
Showing 12 changed files with 1,026 additions and 2 deletions.
4 changes: 3 additions & 1 deletion apis/offline/v1alpha1/addofflinelicense_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
)

// +genclient
// +genclient:nonNamespaced
// +genclient:onlyVerbs=create
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

Expand All @@ -40,7 +41,8 @@ type AddOfflineLicense struct {
}

type AddOfflineLicenseRequest struct {
License string `json:"license"`
Namespace string `json:"namespace"`
License string `json:"license"`
}

type AddOfflineLicenseResponse struct {
Expand Down
9 changes: 8 additions & 1 deletion apis/offline/v1alpha1/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions artifacts/addlicense.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: offline.licenses.appscode.com/v1alpha1
kind: AddOfflineLicense

request:
namespace: kubeops
license: |
-----BEGIN CERTIFICATE-----
MIIEUDCCAzigAwIBAgIIc/mtiBJRhtUwDQYJKoZIhvcNAQELBQAwJTEWMBQGA1UE
ChMNQXBwc0NvZGUgSW5jLjELMAkGA1UEAxMCY2EwHhcNMjQwNTE1MTA1NDQwWhcN
MjQwNjE0MTA1NDQwWjCCARgxDzANBgNVBAYTBmt1YmVkYjETMBEGA1UECBMKZW50
ZXJwcmlzZTGBpDAXBgNVBAoTEGt1YmVkYi1jb21tdW5pdHkwFwYDVQQKExBrdWJl
ZGItZXh0LXN0YXNoMBgGA1UEChMRa3ViZWRiLWF1dG9zY2FsZXIwGAYDVQQKExFr
dWJlZGItZW50ZXJwcmlzZTAcBgNVBAoTFXBhbm9wdGljb24tZW50ZXJwcmlzZTAe
BgNVBAoTF2t1YmVkYi1tb25pdG9yaW5nLWFnZW50MRowGAYDVQQLExFrdWJlZGIt
ZW50ZXJwcmlzZTEtMCsGA1UEAxMkZTg2NTk1OWEtMjk2ZS00OWVhLWI5NjktNGZh
MmRkMGU2NTBmMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApBsr5C40
3IMzdy/EA1pvYO2RCaf9xmwfcI5aofWbox821UgTzwA1OqwXByh7ANQNPAcvPQTn
2NMBoqdjdVQ/PHuHP8vTVvcXjS07g061HGW4LuEXYwoxd8ANBm8wKSdaHcH2KqBm
PdqNAST2r5gPj+vwdbr3KQkMNA6xJK9fNe/Ho1UoWc7/N1ex+HQdV+iBT+wYDIwm
VxPVy5mUdxMx+mJMLBehgVs/tdDHqc619+vXzl9hKXtKczzzcpEM9DOjjkfHCdMa
f9Y+7XeRlZ3659DQowwVVAYXi6oYv4Dn7ahXpPR5KBZMQu290pUgvTA+4AJjFjIF
AkIxiQndHbtPMQIDAQABo4GOMIGLMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAK
BggrBgEFBQcDAjBkBgNVHREEXTBbgiRlODY1OTU5YS0yOTZlLTQ5ZWEtYjk2OS00
ZmEyZGQwZTY1MGaBH1RhbWFsIFNhaGEgPHRhbWFsQGFwcHNjb2RlLmNvbT6BEnRh
bWFsQGFwcHNjb2RlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAJ+vBtrzfDhtndWHn
kcJ9wgKfDWtW1kfhTPvIPO+YFV1vHHGyxuS6TwMNCsiXwAMu5Qao+pWCUrV0CfCC
p405HtO9aHfSPEz2YcoqanIevvaUzDP6DeUW6BCE4LwaPOdCtiUdLx57F7yCu7K4
SQLZv9ufIdyZ9oDz5hvNuPWtPorB0/rgHWPxFmOJfVdCxp1gKqN/QJcwicC75P7U
QcwYQFpsGkSYZijKrB1fGmULf+p3ig6+JKXdPw27K/rnDaP8emjVYwoUibAvL8O1
u0FfA5lbD0X/VLBPz+Czvsunn6ESZCEba7TQUEv6S4rfuRdKFFVj4ljza+W75DH7
OKfieg==
-----END CERTIFICATE-----
17 changes: 17 additions & 0 deletions pkg/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import (
costapi "kubeops.dev/ui-server/apis/cost/v1alpha1"
identityinstall "kubeops.dev/ui-server/apis/identity/install"
identityv1alpha1 "kubeops.dev/ui-server/apis/identity/v1alpha1"
licenseinstall "kubeops.dev/ui-server/apis/offline/install"
licenseapi "kubeops.dev/ui-server/apis/offline/v1alpha1"
policyinstall "kubeops.dev/ui-server/apis/policy/install"
policyapi "kubeops.dev/ui-server/apis/policy/v1alpha1"
projectquotacontroller "kubeops.dev/ui-server/pkg/controllers/projectquota"
Expand Down Expand Up @@ -61,6 +63,8 @@ import (
"kubeops.dev/ui-server/pkg/registry/meta/resourcetabledefinition"
"kubeops.dev/ui-server/pkg/registry/meta/usermenu"
"kubeops.dev/ui-server/pkg/registry/meta/vendormenu"
"kubeops.dev/ui-server/pkg/registry/offline/addofflinelicense"
"kubeops.dev/ui-server/pkg/registry/offline/offlinelicense"
policystorage "kubeops.dev/ui-server/pkg/registry/policy/reports"
imagestorage "kubeops.dev/ui-server/pkg/registry/scanner/image"
reportstorage "kubeops.dev/ui-server/pkg/registry/scanner/reports"
Expand Down Expand Up @@ -125,6 +129,7 @@ func init() {
rscoreinstall.Install(Scheme)
mgmtinstall.Install(Scheme)
crdinstall.Install(Scheme)
licenseinstall.Install(Scheme)
utilruntime.Must(scannerscheme.AddToScheme(Scheme))
utilruntime.Must(chartsapi.AddToScheme(Scheme))
utilruntime.Must(clientgoscheme.AddToScheme(Scheme))
Expand Down Expand Up @@ -310,6 +315,18 @@ func (c completedConfig) New(ctx context.Context) (*UIServer, error) {
return nil, err
}
}
{
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(licenseapi.GroupName, Scheme, metav1.ParameterCodec, Codecs)

v1alpha1storage := map[string]rest.Storage{}
v1alpha1storage[licenseapi.ResourceOfflineLicenses] = offlinelicense.NewStorage(ctrlClient)
v1alpha1storage[licenseapi.ResourceAddOfflineLicenses] = addofflinelicense.NewStorage(ctrlClient, cid, rbacAuthorizer)
apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage

if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
}
{
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(auditor.GroupName, Scheme, metav1.ParameterCodec, Codecs)

Expand Down
5 changes: 5 additions & 0 deletions pkg/cmds/server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
reportsapi "kubeops.dev/scanner/apis/reports/v1alpha1"
costapi "kubeops.dev/ui-server/apis/cost/v1alpha1"
identityv1alpha1 "kubeops.dev/ui-server/apis/identity/v1alpha1"
licenseapi "kubeops.dev/ui-server/apis/offline/v1alpha1"
policyapi "kubeops.dev/ui-server/apis/policy/v1alpha1"
"kubeops.dev/ui-server/pkg/apiserver"
featurecontroller "kubeops.dev/ui-server/pkg/controllers/feature"
Expand Down Expand Up @@ -159,6 +160,10 @@ func (o *UIServerOptions) Config() (*apiserver.Config, error) {
fmt.Sprintf("/apis/%s/%s", rsapi.SchemeGroupVersion, rsapi.ResourceResourceTableDefinitions),

fmt.Sprintf("/apis/%s/%s", rscoreapi.SchemeGroupVersion, rscoreapi.ResourceProjects),

fmt.Sprintf("/apis/%s", licenseapi.SchemeGroupVersion),
fmt.Sprintf("/apis/%s/%s", licenseapi.SchemeGroupVersion, licenseapi.ResourceAddOfflineLicenses),
fmt.Sprintf("/apis/%s/%s", licenseapi.SchemeGroupVersion, licenseapi.ResourceOfflineLicenses),
}

serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
Expand Down
216 changes: 216 additions & 0 deletions pkg/registry/offline/addofflinelicense/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
Copyright AppsCode Inc. and Contributors.
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 addofflinelicense

import (
"context"
"errors"
"fmt"
"strings"

licenseapi "kubeops.dev/ui-server/apis/offline/v1alpha1"

core "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authorization/authorizer"
apirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/client-go/util/cert"
cg "kmodules.xyz/client-go/client"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
LicenseSecretName = "license-proxyserver-licenses"
)

var secretGR = schema.GroupResource{
Group: "",
Resource: "secrets",
}

type Storage struct {
kc client.Client
clusterID string
a authorizer.Authorizer
}

var (
_ rest.GroupVersionKindProvider = &Storage{}
_ rest.Scoper = &Storage{}
_ rest.Storage = &Storage{}
_ rest.Creater = &Storage{}
_ rest.SingularNameProvider = &Storage{}
)

func NewStorage(kc client.Client, clusterID string, a authorizer.Authorizer) *Storage {
return &Storage{
kc: kc,
clusterID: clusterID,
a: a,
}
}

func (r *Storage) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind {
return licenseapi.SchemeGroupVersion.WithKind(licenseapi.ResourceKindAddOfflineLicense)
}

func (r *Storage) NamespaceScoped() bool {
return false
}

func (r *Storage) GetSingularName() string {
return strings.ToLower(licenseapi.ResourceKindAddOfflineLicense)
}

func (r *Storage) New() runtime.Object {
return &licenseapi.AddOfflineLicense{}
}

func (r *Storage) Destroy() {}

func (r *Storage) Create(ctx context.Context, obj runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) {
in := obj.(*licenseapi.AddOfflineLicense)
if in.Request == nil {
return nil, apierrors.NewBadRequest("missing apirequest")
}
req := in.Request

user, ok := apirequest.UserFrom(ctx)
if !ok {
return nil, apierrors.NewBadRequest("missing user info")
}

if req.Namespace == "" {
return nil, apierrors.NewBadRequest("missing license secret namespace")
}
if req.License == "" {
return nil, apierrors.NewBadRequest("missing license info")
}

licenseSecret := v1.Secret{}
err := r.kc.Get(ctx, types.NamespacedName{Name: LicenseSecretName, Namespace: req.Namespace}, &licenseSecret)
if err != nil && apierrors.IsNotFound(err) {
// check permission
attrs := authorizer.AttributesRecord{
User: user,
Verb: "create",
Namespace: req.Namespace,
APIGroup: secretGR.Group,
Resource: secretGR.Resource,
Name: LicenseSecretName,
ResourceRequest: true,
}
decision, why, err := r.a.Authorize(ctx, attrs)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
if decision != authorizer.DecisionAllow {
return nil, apierrors.NewForbidden(secretGR, LicenseSecretName, errors.New(why))
}

productKey, err := getProductKey([]byte(req.License), r.clusterID)
if err != nil {
return nil, err
}

licenseSecret = v1.Secret{
ObjectMeta: controllerruntime.ObjectMeta{
Name: LicenseSecretName,
Namespace: req.Namespace,
},
Data: map[string][]byte{
productKey: []byte(req.License),
},
}
if err = r.kc.Create(ctx, &licenseSecret); err != nil {
return nil, err
}

in.Response = &licenseapi.AddOfflineLicenseResponse{
SecretKeyRef: &core.SecretKeySelector{
LocalObjectReference: core.LocalObjectReference{
Name: licenseSecret.Name,
},
Key: productKey,
},
}
return in, nil
} else if err != nil {
return nil, err
}

// check permission
attrs := authorizer.AttributesRecord{
User: user,
Verb: "patch",
Namespace: req.Namespace,
APIGroup: secretGR.Group,
Resource: secretGR.Resource,
Name: LicenseSecretName,
ResourceRequest: true,
}
decision, why, err := r.a.Authorize(ctx, attrs)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
if decision != authorizer.DecisionAllow {
return nil, apierrors.NewForbidden(secretGR, LicenseSecretName, errors.New(why))
}

productKey, err := getProductKey([]byte(req.License), r.clusterID)
if err != nil {
return nil, err
}
licenseSecret.Data[productKey] = []byte(req.License)

_, err = cg.CreateOrPatch(ctx, r.kc, &licenseSecret, func(obj client.Object, createOp bool) client.Object {
in := obj.(*v1.Secret)
in.Data = licenseSecret.Data
return in
})
if err != nil {
return nil, err
}

in.Response = &licenseapi.AddOfflineLicenseResponse{
SecretKeyRef: &core.SecretKeySelector{
LocalObjectReference: core.LocalObjectReference{
Name: licenseSecret.Name,
},
Key: productKey,
},
}
return in, nil
}

func getProductKey(lic []byte, clusterID string) (string, error) {
certs, err := cert.ParseCertsPEM(lic)
if err != nil {
return "", err
}
if certs[0].Subject.CommonName != clusterID {
return "", fmt.Errorf("license is for cluster %s, expecting %s", certs[0].Subject.CommonName, clusterID)
}
return certs[0].Subject.OrganizationalUnit[0], nil
}
Loading

0 comments on commit 01a6d42

Please sign in to comment.