diff --git a/controllers/pkg/go.mod b/controllers/pkg/go.mod index 23243101..82581422 100644 --- a/controllers/pkg/go.mod +++ b/controllers/pkg/go.mod @@ -15,7 +15,7 @@ require ( github.com/go-logr/logr v1.4.2 github.com/google/go-cmp v0.6.0 github.com/henderiw-nephio/network v0.0.0-20230626193806-04743403261e - github.com/nephio-project/api v1.0.0 + github.com/nephio-project/api v1.0.1-0.20231127124455-cf14bd57b08d github.com/nephio-project/nephio/krm-functions/configinject-fn v0.0.0-00010101000000-000000000000 github.com/nephio-project/nephio/krm-functions/ipam-fn v0.0.0-00010101000000-000000000000 github.com/nephio-project/nephio/krm-functions/lib v0.0.0-20230605213956-a1e470f419a4 @@ -23,7 +23,6 @@ require ( github.com/nephio-project/nephio/testing/mockeryutils v0.0.0-20240112001535-96b08ff4acb3 github.com/nephio-project/porch v1.3.1 github.com/nokia/k8s-ipam v0.0.4-0.20230628092530-8a292aec80a4 - github.com/openconfig/ygot v0.28.3 github.com/pkg/errors v0.9.1 github.com/srl-labs/ygotsrl/v22 v22.11.1 github.com/stretchr/testify v1.9.0 @@ -37,8 +36,16 @@ require ( sigs.k8s.io/yaml v1.4.0 ) +require ( + github.com/openconfig/ygot v0.28.3 + github.com/spiffe/go-spiffe/v2 v2.3.0 +) + +require github.com/kylelemons/godebug v1.1.0 // indirect + require ( github.com/GoogleContainerTools/kpt-functions-sdk/go/api v0.0.0-20230427202446-3255accc518d // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -48,6 +55,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-fed/httpsig v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect @@ -64,7 +72,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kentik/patricia v1.2.0 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -81,6 +88,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/xlab/treeprint v1.2.0 // indirect + github.com/zeebo/errs v1.3.0 // indirect go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect @@ -95,7 +103,7 @@ require ( google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.30.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/controllers/pkg/go.sum b/controllers/pkg/go.sum index 60b189a5..e7ce6126 100644 --- a/controllers/pkg/go.sum +++ b/controllers/pkg/go.sum @@ -6,6 +6,8 @@ github.com/GoogleContainerTools/kpt-functions-sdk/go/api v0.0.0-20230427202446-3 github.com/GoogleContainerTools/kpt-functions-sdk/go/api v0.0.0-20230427202446-3255accc518d/go.mod h1:prNhhUAODrB2VqHVead9tB8nLU9ffY4e4jjBwLMNO1M= github.com/GoogleContainerTools/kpt-functions-sdk/go/fn v0.0.0-20230427202446-3255accc518d h1:kgC/R6Kl+tBjsRvcPr4Beae1MiHumNMtbmUTy7qlPZI= github.com/GoogleContainerTools/kpt-functions-sdk/go/fn v0.0.0-20230427202446-3255accc518d/go.mod h1:Pnd3ImgaWS3OBVjztSiGMACMf+CDs20l5nT5Oljy/tA= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= @@ -34,6 +36,8 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -122,8 +126,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nephio-project/api v1.0.0 h1:uHu2SP2nHpx7B9if0MZrw5BqAKBf2dG2Ssp6bp6R6lY= -github.com/nephio-project/api v1.0.0/go.mod h1:9w+JbXeyiT3KZrrXab0pzaWtiUk4upvgLzpqOtSmbpI= +github.com/nephio-project/api v1.0.1-0.20231127124455-cf14bd57b08d h1:hs1ml1d3MaLBLn5yhfz2RPA9B8VGXMGJhZSjyDXJDQY= +github.com/nephio-project/api v1.0.1-0.20231127124455-cf14bd57b08d/go.mod h1:9w+JbXeyiT3KZrrXab0pzaWtiUk4upvgLzpqOtSmbpI= github.com/nephio-project/nephio/testing/mockeryutils v0.0.0-20240112001535-96b08ff4acb3 h1:RNwnrA6AmFLFZkmJa6rVX6PTpf4QxlCF5oYWdpsap1g= github.com/nephio-project/nephio/testing/mockeryutils v0.0.0-20240112001535-96b08ff4acb3/go.mod h1:mQqKgxdpWotKvgZKbfFHPK0gLJ4Z9CsJb/tEUoeDpLs= github.com/nephio-project/porch v1.3.1 h1:CFZa6kaLViGU7vGMy9sgGGvom5HUxwGwyOiz5zfuCZ8= @@ -164,6 +168,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8= +github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY= github.com/srl-labs/ygotsrl/v22 v22.11.1 h1:Dxb7q7IB8xZc0XOZC53ZPBATxA8dJ+oJMC+2FYToId8= github.com/srl-labs/ygotsrl/v22 v22.11.1/go.mod h1:VuNY6D0aYZvR9UeGSWOzgATBsis3ynw84TwiYuhS+pc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -182,6 +188,8 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= +github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/controllers/pkg/reconcilers/spire-bootstrap/kubeconfigTypes.go b/controllers/pkg/reconcilers/spire-bootstrap/kubeconfigTypes.go new file mode 100644 index 00000000..94d20033 --- /dev/null +++ b/controllers/pkg/reconcilers/spire-bootstrap/kubeconfigTypes.go @@ -0,0 +1,40 @@ +package spirebootstrap + +type KubernetesConfig struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Clusters []Cluster `yaml:"clusters"` + Contexts []Context `yaml:"contexts"` + Users []User `yaml:"users"` + CurrentContext string `yaml:"current-context"` +} + +type Cluster struct { + Name string `yaml:"name"` + Cluster ClusterDetail `yaml:"cluster"` +} + +type ClusterDetail struct { + CertificateAuthorityData string `yaml:"certificate-authority-data"` + Server string `yaml:"server"` +} + +type Context struct { + Name string `yaml:"name"` + Context ContextDetails `yaml:"context"` +} + +type ContextDetails struct { + Cluster string `yaml:"cluster"` + Namespace string `yaml:"namespace"` + User string `yaml:"user"` +} + +type User struct { + Name string `yaml:"name"` + User UserDetail `yaml:"user"` +} + +type UserDetail struct { + Token string `yaml:"token"` +} diff --git a/controllers/pkg/reconcilers/spire-bootstrap/reconciler.go b/controllers/pkg/reconcilers/spire-bootstrap/reconciler.go new file mode 100644 index 00000000..0eb22533 --- /dev/null +++ b/controllers/pkg/reconcilers/spire-bootstrap/reconciler.go @@ -0,0 +1,493 @@ +/* +Copyright 2023 The Nephio Authors. + +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 spirebootstrap + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + "time" + + "github.com/nephio-project/nephio/controllers/pkg/cluster" + reconcilerinterface "github.com/nephio-project/nephio/controllers/pkg/reconcilers/reconciler-interface" + "github.com/nephio-project/nephio/controllers/pkg/resource" + "gopkg.in/yaml.v2" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + capiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func init() { + reconcilerinterface.Register("workloadidentity", &reconciler{}) +} + +type LoginPayload struct { + Role string `json:"role"` + JWT string `json:"jwt"` +} + +type AuthResponse struct { + Auth struct { + ClientToken string `json:"client_token"` + } `json:"auth"` +} + +//+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch +//+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters/status,verbs=get + +// SetupWithManager sets up the controller with the Manager. +func (r *reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, c any) (map[schema.GroupVersionKind]chan event.GenericEvent, error) { + r.Client = mgr.GetClient() + + return nil, ctrl.NewControllerManagedBy(mgr). + Named("BootstrapSpireController"). + For(&capiv1beta1.Cluster{}). + Complete(r) +} + +type reconciler struct { + client.Client +} + +// r.List --> gets us cluster name list + +func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + cl := &capiv1beta1.Cluster{} + err := r.Get(ctx, req.NamespacedName, cl) + if err != nil { + if client.IgnoreNotFound(err) != nil { + log.Error(err, "unable to fetch Cluster") + } + return reconcile.Result{}, client.IgnoreNotFound(err) + } + + // Add your reconciliation logic here + log.Info("Reconciling Cluster", "cluster", cl.Name) + + // Fetch the ConfigMap from the current cluster + configMapName := types.NamespacedName{Name: "spire-bundle", Namespace: "spire"} + configMap := &v1.ConfigMap{} + err = r.Get(ctx, configMapName, configMap) + if err != nil { + log.Error(err, "unable to fetch ConfigMap") + return reconcile.Result{}, err + } + + secrets := &v1.SecretList{} + if err := r.List(ctx, secrets); err != nil { + msg := "cannot list secrets" + log.Error(err, msg) + return ctrl.Result{}, errors.Wrap(err, msg) + } + + // Get the spire-server service + spireService := &v1.Service{} + err = r.Get(ctx, types.NamespacedName{Name: "spire-server", Namespace: "spire"}, spireService) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get spire-server service: %v", err) + } + + // Get the ClusterIP + clusterIP := spireService.Spec.ClusterIP + + // Get the port + var port string + if len(spireService.Spec.Ports) > 0 { + port = fmt.Sprint(spireService.Spec.Ports[0].Port) + } + + // Construct the service address + serviceAddress := fmt.Sprintf("%s:%s", clusterIP, port) + + spireAgentCM, err := createSpireAgentConfigMap("spire-agent", "spire", cl.Name, clusterIP, port) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get spireAgent ConfigMap: %v", err) + } + + for _, secret := range secrets.Items { + if strings.Contains(secret.GetName(), cl.Name) { + secret := secret // required to prevent gosec warning: G601 (CWE-118): Implicit memory aliasing in for loop + clusterClient, ok := cluster.Cluster{Client: r.Client}.GetClusterClient(&secret) + if ok { + + jwtSVID, err := resource.GetJWT(ctx) + if err != nil { + log.Error(err, "Unable to get jwtSVID") + } + fmt.Println("JWTTT: ", jwtSVID) + + createK8sSATokenResources(clusterClient, ctx) + if err != nil { + fmt.Println("Error creating K8s", err) + } + + // kubeconfigCM, err := r.createKubeconfigConfigMap(ctx, clusterClient, cl.Name) + // if err != nil { + // fmt.Println("Error creating K8s kubeconfig configmap", err) + // } + + // r.Update(ctx, kubeconfigCM) + + // err = updateClusterListConfigMap(clusterClient, cl.Name) + // if err != nil { + // fmt.Println("Cluster list could not be updated...: ", err) + // } + + fmt.Printf("SPIRE Server service address: %s\n", serviceAddress) + + clusterClient, ready, err := clusterClient.GetClusterClient(ctx) + if err != nil { + msg := "cannot get clusterClient" + log.Error(err, msg) + return ctrl.Result{RequeueAfter: 30 * time.Second}, errors.Wrap(err, msg) + } + if !ready { + log.Info("cluster not ready") + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + + remoteNamespace := configMap.Namespace + ns := &v1.Namespace{} + if err = clusterClient.Get(ctx, types.NamespacedName{Name: remoteNamespace}, ns); err != nil { + if resource.IgnoreNotFound(err) != nil { + msg := fmt.Sprintf("cannot get namespace: %s", remoteNamespace) + log.Error(err, msg) + return ctrl.Result{RequeueAfter: 30 * time.Second}, errors.Wrap(err, msg) + } + msg := fmt.Sprintf("namespace: %s, does not exist, retry...", remoteNamespace) + log.Info(msg) + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + + newcr := configMap.DeepCopy() + + newcr.ResourceVersion = "" + newcr.UID = "" + newcr.Namespace = remoteNamespace + + newAgentConf := spireAgentCM.DeepCopy() + newAgentConf.ResourceVersion = "" + newAgentConf.UID = "" + newAgentConf.Namespace = remoteNamespace + log.Info("secret info", "secret", newcr.Annotations) + log.Info("configMap info", "configMap", newAgentConf.Annotations) + if err := clusterClient.Apply(ctx, newcr); err != nil { + msg := fmt.Sprintf("cannot apply secret to cluster %s", cl.Name) + log.Error(err, msg) + return ctrl.Result{}, errors.Wrap(err, msg) + } + if err := clusterClient.Apply(ctx, newAgentConf); err != nil { + msg := fmt.Sprintf("cannot apply ConfigMap to cluster %s", cl.Name) + log.Error(err, msg) + return ctrl.Result{}, errors.Wrap(err, msg) + } + } + } + + } + + return reconcile.Result{}, nil +} + +func createK8sSATokenResources(clusterClient cluster.ClusterClient, ctx context.Context) error { + log := log.FromContext(ctx) + + client, ready, err := clusterClient.GetClusterClient(ctx) + if err != nil { + msg := "cannot get clusterClient" + log.Error(err, msg) + return errors.Wrap(err, msg) + } + if !ready { + log.Info("cluster not ready") + return nil + } + + // Create ServiceAccount + sa := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spire-kubeconfig", + Namespace: "spire", + }, + } + if err := client.Create(ctx, sa); err != nil { + return fmt.Errorf("failed to create ServiceAccount: %v", err) + } + + // Create ClusterRole + cr := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-reader", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods", "nodes"}, + Verbs: []string{"get"}, + }, + }, + } + if err := client.Create(ctx, cr); err != nil { + return fmt.Errorf("failed to create ClusterRole: %v", err) + } + + // Create ClusterRoleBinding for system:auth-delegator + crbAuthDelegator := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spire-agent-tokenreview-binding", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: "spire-kubeconfig", + Namespace: "spire", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "system:auth-delegator", + }, + } + if err := client.Create(ctx, crbAuthDelegator); err != nil { + return fmt.Errorf("failed to create ClusterRoleBinding (auth-delegator): %v", err) + } + + // Create ClusterRoleBinding for pod-reader + crbPodReader := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "spire-agent-pod-reader-binding", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: "spire-kubeconfig", + Namespace: "spire", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "pod-reader", + }, + } + if err := client.Create(ctx, crbPodReader); err != nil { + return fmt.Errorf("failed to create ClusterRoleBinding (pod-reader): %v", err) + } + + // Create Secret + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "agent-sa-secret", + Namespace: "spire", + Annotations: map[string]string{ + "kubernetes.io/service-account.name": "spire-kubeconfig", + }, + }, + Type: v1.SecretTypeServiceAccountToken, + } + if err := client.Create(ctx, secret); err != nil { + return fmt.Errorf("failed to create Secret: %v", err) + } + + return nil +} + +func (r *reconciler) createKubeconfigConfigMap(ctx context.Context, clientset *kubernetes.Clientset, clustername string) (*v1.ConfigMap, error) { + + cmName := types.NamespacedName{Name: "kubeconfigs", Namespace: "spire"} + restrictedKC := &v1.ConfigMap{} + err := r.Get(ctx, cmName, restrictedKC) + if err != nil { + return nil, fmt.Errorf("failed to get existing ConfigMap: %v", err) + } + + // Retrieve the ServiceAccount token + secret, err := clientset.CoreV1().Secrets("spire").Get(context.TODO(), "agent-sa-secret", metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get ServiceAccount token: %v", err) + } + token := string(secret.Data["token"]) + + // Retrieve the cluster's CA certificate + configMap, err := clientset.CoreV1().ConfigMaps("kube-system").Get(context.TODO(), "kube-root-ca.crt", metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get cluster CA certificate: %v", err) + } + caCert := configMap.Data["ca.crt"] + caCertEncoded := strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(caCert))) + + config := KubernetesConfig{ + APIVersion: "v1", + Kind: "Config", + Clusters: []Cluster{ + { + Name: clustername, + Cluster: ClusterDetail{ + CertificateAuthorityData: caCertEncoded, + Server: clientset.RESTClient().Get().URL().String(), + }, + }, + }, + Contexts: []Context{ + { + Name: "spire-kubeconfig@" + clustername, + Context: ContextDetails{ + Cluster: clustername, + Namespace: "spire", + User: "spire-kubeconfig", + }, + }, + }, + Users: []User{ + { + Name: "spire-kubeconfig", + User: UserDetail{ + Token: token, + }, + }, + }, + CurrentContext: "spire-kubeconfig@" + clustername, + } + + // Convert to YAML + yamlData, err := yaml.Marshal(&config) + if err != nil { + return nil, fmt.Errorf("failed to create kubeconfig ConfigMap: %v", err) + } + + // Generate a unique key for the new kubeconfig + newConfigKey := fmt.Sprintf("kubeconfig-%s", clustername) + + // Add the new kubeconfig to the existing ConfigMap + if restrictedKC.Data == nil { + restrictedKC.Data = make(map[string]string) + } + restrictedKC.Data[newConfigKey] = string(yamlData) + + err = r.Update(ctx, restrictedKC) + if err != nil { + return nil, fmt.Errorf("failed to create kubeconfig ConfigMap: %v", err) + } + + return restrictedKC, nil +} + +func updateClusterListConfigMap(clientset *kubernetes.Clientset, clusterName string) error { + + // Get the ConfigMap + cm, err := clientset.CoreV1().ConfigMaps("spire").Get(context.TODO(), "clusters", metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("error getting ConfigMap: %v", err) + } + + // Get the clusters.conf data + clustersConf, ok := cm.Data["clusters.conf"] + if !ok { + return fmt.Errorf("clusters.conf not found in ConfigMap") + } + + // Add new cluster + newCluster := fmt.Sprintf(` + "%s" = { + service_account_allow_list = ["spire:spire-agent"] + kube_config_file = "/run/spire/kubeconfigs/kubeconfig-%s" + }`, clusterName, clusterName) + + // Insert the new cluster before the last closing brace + lastBraceIndex := strings.LastIndex(clustersConf, "}") + if lastBraceIndex != -1 { + clustersConf = clustersConf[:lastBraceIndex] + newCluster + clustersConf[lastBraceIndex:] + } else { + return fmt.Errorf("invalid clusters.conf format") + } + + // Update the ConfigMap + cm.Data["clusters.conf"] = clustersConf + + // Apply the changes + _, err = clientset.CoreV1().ConfigMaps("spire").Update(context.TODO(), cm, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("error updating ConfigMap: %v", err) + } + + return nil +} + +func createSpireAgentConfigMap(name string, namespace string, cluster string, serverAddress string, serverPort string) (*v1.ConfigMap, error) { + configMapData := map[string]string{ + "agent.conf": ` +agent { + data_dir = "/run/spire" + log_level = "DEBUG" + server_address = "` + serverAddress + `" + server_port = "` + serverPort + `" + socket_path = "/run/spire/sockets/spire-agent.sock" + trust_bundle_path = "/run/spire/bundle/bundle.crt" + trust_domain = "example.org" +} + +plugins { + NodeAttestor "k8s_psat" { + plugin_data { + cluster = "` + cluster + `" + } + } + + KeyManager "memory" { + plugin_data { + } + } + + WorkloadAttestor "k8s" { + plugin_data { + skip_kubelet_verification = true + } + } +} +`, + } + + configMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: configMapData, + } + + // createdConfigMap, err := clientset.CoreV1().ConfigMaps(namespace).Create(context.TODO(), configMap, metav1.CreateOptions{}) + // if err != nil { + // return nil, err + // } + + return configMap, nil +} diff --git a/controllers/pkg/resource/workloadapi.go b/controllers/pkg/resource/workloadapi.go new file mode 100644 index 00000000..67be931e --- /dev/null +++ b/controllers/pkg/resource/workloadapi.go @@ -0,0 +1,60 @@ +package resource + +import ( + "context" + "fmt" + + "github.com/spiffe/go-spiffe/v2/workloadapi" + + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func GetJWT(ctx context.Context) (*jwtsvid.SVID, error) { + socketPath := "unix:///spiffe-workload-api/spire-agent.sock" + log := log.FromContext(ctx) + clientOptions := workloadapi.WithClientOptions(workloadapi.WithAddr(socketPath)) + jwtSource, err := workloadapi.NewJWTSource(ctx, clientOptions) + if err != nil { + log.Info("Unable to create JWTSource: %v", err) + } + defer jwtSource.Close() + + audience := "TESTING" + spiffeID := spiffeid.RequireFromString("spiffe://example.org/nephio") + + jwtSVID, err := jwtSource.FetchJWTSVID(ctx, jwtsvid.Params{ + Audience: audience, + Subject: spiffeID, + }) + if err != nil { + log.Info("Unable to fetch JWT-SVID: %v", err) + } + + fmt.Printf("Fetched JWT-SVID: %v\n", jwtSVID.Marshal()) + if err != nil { + log.Error(err, "Spire auth didnt work") + } + + return jwtSVID, err +} + +type Watcher struct{} + +func (Watcher) OnX509ContextUpdate(x509Context *workloadapi.X509Context) { + fmt.Println("Update:") + fmt.Println(" SVIDs:") + for _, svid := range x509Context.SVIDs { + fmt.Printf(" %s\n", svid.ID) + } + fmt.Println(" Bundles:") + for _, bundle := range x509Context.Bundles.Bundles() { + fmt.Printf(" %s (%d authorities)\n", bundle.TrustDomain(), len(bundle.X509Authorities())) + } +} + +func (Watcher) OnX509ContextWatchError(err error) { + fmt.Println("Error:", err) +} diff --git a/operators/nephio-controller-manager/main.go b/operators/nephio-controller-manager/main.go index 9060cd64..5e38d0a7 100644 --- a/operators/nephio-controller-manager/main.go +++ b/operators/nephio-controller-manager/main.go @@ -42,6 +42,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + capiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" ctrl "sigs.k8s.io/controller-runtime" //+kubebuilder:scaffold:imports @@ -53,6 +54,7 @@ import ( _ "github.com/nephio-project/nephio/controllers/pkg/reconcilers/generic-specializer" _ "github.com/nephio-project/nephio/controllers/pkg/reconcilers/network" _ "github.com/nephio-project/nephio/controllers/pkg/reconcilers/repository" + _ "github.com/nephio-project/nephio/controllers/pkg/reconcilers/spire-bootstrap" _ "github.com/nephio-project/nephio/controllers/pkg/reconcilers/token" ) @@ -95,6 +97,13 @@ func main() { os.Exit(1) } + err = capiv1beta1.AddToScheme(scheme) + if err != nil { + setupLog.Error(err, "cannot initializer schema with Cluster API(s)") + // klog.Errorf("error initializing scheme with Porch APIs: %s", err.Error()) + os.Exit(1) + } + managerOptions := ctrl.Options{ Scheme: scheme, Metrics: server.Options{