diff --git a/pkg/fleet-manager/backup/render/manifests/rbac.tpl b/pkg/fleet-manager/backup/render/manifests/rbac.tpl new file mode 100644 index 000000000..5d3b89970 --- /dev/null +++ b/pkg/fleet-manager/backup/render/manifests/rbac.tpl @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: "{{ .ServiceAccountName }}" + namespace: "{{ .PipelineNamespace }}" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "{{ .RoleBindingName }}" + namespace: "{{ .PipelineNamespace }}" +subjects: + - kind: ServiceAccount + name: "{{ .ServiceAccountName }}" + namespace: "{{ .PipelineNamespace }}" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tekton-triggers-eventlistener-roles # add role for handle EventListener, configmap, secret and so on. `tekton-triggers-eventlistener-roles` is provided by Tekton +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: "{{ .ClusterRoleBindingName }}" +subjects: + - kind: ServiceAccount + name: "{{ .ServiceAccountName }}" + namespace: "{{ .PipelineNamespace }}" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tekton-triggers-eventlistener-clusterroles # add role for handle pod. `tekton-triggers-eventlistener-clusterroles` is provided by Tekton diff --git a/pkg/fleet-manager/backup/render/rbac.go b/pkg/fleet-manager/backup/render/rbac.go new file mode 100644 index 000000000..298807d50 --- /dev/null +++ b/pkg/fleet-manager/backup/render/rbac.go @@ -0,0 +1,61 @@ +/* +Copyright Kurator 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 render + +import ( + "fmt" + "io/fs" +) + +const ( + ServiceAccountNamePrefix = "kurator-pipeline-robot-" + RoleBindingNameSuffix = "-binding" + ClusterRoleBindingNameSuffix = "-clusterbinding" + // RBACTemplateFileName is the name of the RBAC template file. + RBACTemplateFileName = "rbac.tpl" + RBACTemplateName = "pipeline rbac template" +) + +// RBACConfig contains the configuration data required for the RBAC template. +// Both PipelineName and PipelineNamespace are required. +type RBACConfig struct { + PipelineName string // Name of the pipeline. + PipelineNamespace string // Kubernetes namespace where the pipeline is deployed. +} + +// ServiceAccountName generates the service account name using the pipeline name and namespace. +func (rbac RBACConfig) ServiceAccountName() string { + return ServiceAccountNamePrefix + rbac.PipelineName + "-" + rbac.PipelineNamespace +} + +// RoleBindingName generates the role binding name using the service account name. +func (rbac RBACConfig) RoleBindingName() string { + return rbac.ServiceAccountName() + RoleBindingNameSuffix +} + +// ClusterRoleBindingName generates the cluster role binding name using the service account name. +func (rbac RBACConfig) ClusterRoleBindingName() string { + return rbac.ServiceAccountName() + ClusterRoleBindingNameSuffix +} + +// renderRBAC renders the RBAC configuration using a specified template. +func renderRBAC(fsys fs.FS, cfg RBACConfig) ([]byte, error) { + if cfg.PipelineName == "" || cfg.PipelineNamespace == "" { + return nil, fmt.Errorf("invalid RBACConfig: PipelineName and PipelineNamespace must not be empty") + } + return renderPipelineTemplate(fsys, RBACTemplateFileName, RBACTemplateName, cfg) +} diff --git a/pkg/fleet-manager/backup/render/rbac_test.go b/pkg/fleet-manager/backup/render/rbac_test.go new file mode 100644 index 000000000..8714ef146 --- /dev/null +++ b/pkg/fleet-manager/backup/render/rbac_test.go @@ -0,0 +1,96 @@ +/* +Copyright Kurator 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 render + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "kurator.dev/kurator/pkg/fleet-manager/manifests" +) + +var manifestFS = manifests.BuiltinOrDir("manifests") + +const expectedRBACFilePath = "testdata/rbac/" + +func TestRenderRBAC(t *testing.T) { + // Define test cases including both valid and error scenarios. + cases := []struct { + name string + cfg RBACConfig + expectError bool + expectedFile string + }{ + { + name: "valid configuration", + cfg: RBACConfig{ + PipelineName: "example", + PipelineNamespace: "default", + }, + expectError: false, + expectedFile: "default-example.yaml", + }, + { + name: "empty PipelineName", + cfg: RBACConfig{ + PipelineName: "", + PipelineNamespace: "default", + }, + expectError: true, + }, + { + name: "empty PipelineNamespace", + cfg: RBACConfig{ + PipelineName: "example", + PipelineNamespace: "", + }, + expectError: true, + }, + { + name: "invalid file system path", + cfg: RBACConfig{ + PipelineName: "example", + PipelineNamespace: "default", + }, + expectError: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + fs := manifestFS + // Use an invalid file system for the relevant test case. + if tc.name == "invalid file system path" { + fs = manifests.BuiltinOrDir("invalid-path") + } + + result, err := renderRBAC(fs, tc.cfg) + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + + expected, err := os.ReadFile(expectedRBACFilePath + tc.expectedFile) + assert.NoError(t, err) + assert.Equal(t, string(expected), string(result)) + } + }) + } +} diff --git a/pkg/fleet-manager/backup/render/testdata/rbac/default-example.yaml b/pkg/fleet-manager/backup/render/testdata/rbac/default-example.yaml new file mode 100644 index 000000000..54cee3978 --- /dev/null +++ b/pkg/fleet-manager/backup/render/testdata/rbac/default-example.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: "kurator-pipeline-robot-example-default" + namespace: "default" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "kurator-pipeline-robot-example-default-binding" + namespace: "default" +subjects: + - kind: ServiceAccount + name: "kurator-pipeline-robot-example-default" + namespace: "default" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tekton-triggers-eventlistener-roles # add role for handle EventListener, configmap, secret and so on. `tekton-triggers-eventlistener-roles` is provided by Tekton +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: "kurator-pipeline-robot-example-default-clusterbinding" +subjects: + - kind: ServiceAccount + name: "kurator-pipeline-robot-example-default" + namespace: "default" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tekton-triggers-eventlistener-clusterroles # add role for handle pod. `tekton-triggers-eventlistener-clusterroles` is provided by Tekton diff --git a/pkg/fleet-manager/backup/render/util.go b/pkg/fleet-manager/backup/render/util.go new file mode 100644 index 000000000..d5f410c18 --- /dev/null +++ b/pkg/fleet-manager/backup/render/util.go @@ -0,0 +1,66 @@ +/* +Copyright Kurator 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 render + +import ( + "bytes" + "html/template" + "io/fs" + + "github.com/Masterminds/sprig/v3" + "sigs.k8s.io/yaml" +) + +// renderPipelineTemplate reads, parses, and renders a template file using the provided configuration data. +func renderPipelineTemplate(fsys fs.FS, tplFileName, tplName string, cfg interface{}) ([]byte, error) { + out, err := fs.ReadFile(fsys, tplFileName) + if err != nil { + return nil, err + } + + t := template.New(tplName) + + tpl, err := t.Funcs(funMap()).Parse(string(out)) + if err != nil { + return nil, err + } + + var b bytes.Buffer + + if err := tpl.Execute(&b, cfg); err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// funMap returns a map of functions for use in the template. +func funMap() template.FuncMap { + m := sprig.TxtFuncMap() + m["toYaml"] = toYaml + return m +} + +// toYaml converts a given value to its YAML representation. +func toYaml(value interface{}) string { + y, err := yaml.Marshal(value) + if err != nil { + return "" + } + + return string(y) +}