sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package scaffolds 18 19 import ( 20 "fmt" 21 "path/filepath" 22 "strings" 23 24 log "github.com/sirupsen/logrus" 25 "github.com/spf13/afero" 26 27 "sigs.k8s.io/kubebuilder/v3/pkg/config" 28 "sigs.k8s.io/kubebuilder/v3/pkg/machinery" 29 "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" 30 "sigs.k8s.io/kubebuilder/v3/pkg/plugin" 31 "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" 32 "sigs.k8s.io/kubebuilder/v3/pkg/plugins" 33 kustomizev1scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v1/scaffolds" 34 kustomizev2scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v2/scaffolds" 35 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/api" 36 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/config/samples" 37 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers" 38 golangv3scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" 39 golangv4scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v4/scaffolds" 40 ) 41 42 var _ plugins.Scaffolder = &apiScaffolder{} 43 44 // apiScaffolder contains configuration for generating scaffolding for Go type 45 // representing the API and controller that implements the behavior for the API. 46 type apiScaffolder struct { 47 config config.Config 48 resource resource.Resource 49 image string 50 command string 51 port string 52 runAsUser string 53 54 // fs is the filesystem that will be used by the scaffolder 55 fs machinery.Filesystem 56 } 57 58 // NewDeployImageScaffolder returns a new Scaffolder for declarative 59 // nolint: lll 60 func NewDeployImageScaffolder(config config.Config, res resource.Resource, image, 61 command, port, runAsUser string, 62 ) plugins.Scaffolder { 63 return &apiScaffolder{ 64 config: config, 65 resource: res, 66 image: image, 67 command: command, 68 port: port, 69 runAsUser: runAsUser, 70 } 71 } 72 73 // InjectFS implements cmdutil.Scaffolder 74 func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) { 75 s.fs = fs 76 } 77 78 // Scaffold implements cmdutil.Scaffolder 79 func (s *apiScaffolder) Scaffold() error { 80 log.Println("Writing scaffold for you to edit...") 81 82 //nolint: staticcheck 83 isGoV3 := plugin.IsLegacyLayout(s.config) 84 85 if err := s.scaffoldCreateAPIFromPlugins(isGoV3); err != nil { 86 return err 87 } 88 89 // Load the boilerplate 90 boilerplate, err := afero.ReadFile(s.fs.FS, filepath.Join("hack", "boilerplate.go.txt")) 91 if err != nil { 92 return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err) 93 } 94 95 // Initialize the machinery.Scaffold that will write the files to disk 96 scaffold := machinery.NewScaffold(s.fs, 97 machinery.WithConfig(s.config), 98 machinery.WithBoilerplate(string(boilerplate)), 99 machinery.WithResource(&s.resource), 100 ) 101 102 if err := scaffold.Execute( 103 &api.Types{Port: s.port, IsLegacyLayout: isGoV3}, 104 ); err != nil { 105 return fmt.Errorf("error updating APIs: %v", err) 106 } 107 108 if err := scaffold.Execute( 109 &samples.CRDSample{Port: s.port}, 110 ); err != nil { 111 return fmt.Errorf("error updating config/samples: %v", err) 112 } 113 114 controller := &controllers.Controller{ 115 ControllerRuntimeVersion: golangv3scaffolds.ControllerRuntimeVersion, 116 IsLegacyLayout: isGoV3, 117 } 118 119 if !isGoV3 { 120 controller.ControllerRuntimeVersion = golangv4scaffolds.ControllerRuntimeVersion 121 } 122 123 if err := scaffold.Execute( 124 controller, 125 ); err != nil { 126 return fmt.Errorf("error scaffolding controller: %v", err) 127 } 128 129 if err := s.updateControllerCode(*controller); err != nil { 130 return fmt.Errorf("error updating controller: %v", err) 131 } 132 133 defaultMainPath := "cmd/main.go" 134 if isGoV3 { 135 defaultMainPath = "main.go" 136 } 137 if err := s.updateMainByAddingEventRecorder(defaultMainPath); err != nil { 138 return fmt.Errorf("error updating main.go: %v", err) 139 } 140 141 if err := scaffold.Execute( 142 &controllers.ControllerTest{Port: s.port, IsLegacyLayout: isGoV3}, 143 ); err != nil { 144 return fmt.Errorf("error creating controller/**_controller_test.go: %v", err) 145 } 146 147 if err := s.addEnvVarIntoManager(); err != nil { 148 return err 149 } 150 151 return nil 152 } 153 154 // addEnvVarIntoManager will update the config/manager/manager.yaml by adding 155 // a new ENV VAR for to store the image informed which will be used in the 156 // controller to create the Pod for the Kind 157 func (s *apiScaffolder) addEnvVarIntoManager() error { 158 managerPath := filepath.Join("config", "manager", "manager.yaml") 159 err := util.ReplaceInFile(managerPath, `env:`, `env:`) 160 if err != nil { 161 if err := util.InsertCode(managerPath, `name: manager`, ` 162 env:`); err != nil { 163 return fmt.Errorf("error scaffolding env key in config/manager/manager.yaml") 164 } 165 } 166 167 if err = util.InsertCode(managerPath, `env:`, 168 fmt.Sprintf(envVarTemplate, strings.ToUpper(s.resource.Kind), s.image)); err != nil { 169 return fmt.Errorf("error scaffolding env key in config/manager/manager.yaml") 170 } 171 172 return nil 173 } 174 175 // scaffoldCreateAPIFromPlugins will reuse the code from the kustomize and base golang 176 // plugins to do the default scaffolds which an API is created 177 func (s *apiScaffolder) scaffoldCreateAPIFromPlugins(isLegacyLayout bool) error { 178 if err := s.scaffoldCreateAPIFromGolang(isLegacyLayout); err != nil { 179 return fmt.Errorf("error scaffolding golang files for the new API: %v", err) 180 } 181 182 if err := s.scaffoldCreateAPIFromKustomize(isLegacyLayout); err != nil { 183 return fmt.Errorf("error scaffolding kustomize manifests for the new API: %v", err) 184 } 185 return nil 186 } 187 188 // TODO: replace this implementation by creating its own MainUpdater 189 // which will have its own controller template which set the recorder so that we can use it 190 // in the reconciliation to create an event inside for the finalizer 191 func (s *apiScaffolder) updateMainByAddingEventRecorder(defaultMainPath string) error { 192 if err := util.InsertCode( 193 defaultMainPath, 194 fmt.Sprintf( 195 `%sReconciler{ 196 Client: mgr.GetClient(), 197 Scheme: mgr.GetScheme(),`, s.resource.Kind), 198 fmt.Sprintf(recorderTemplate, strings.ToLower(s.resource.Kind)), 199 ); err != nil { 200 return fmt.Errorf("error scaffolding event recorder in %s: %v", defaultMainPath, err) 201 } 202 203 return nil 204 } 205 206 // updateControllerCode will update the code generate on the template to add the Container information 207 func (s *apiScaffolder) updateControllerCode(controller controllers.Controller) error { 208 if err := util.ReplaceInFile( 209 controller.Path, 210 "//TODO: scaffold container", 211 fmt.Sprintf(containerTemplate, // value for the image 212 strings.ToLower(s.resource.Kind), // value for the name of the container 213 ), 214 ); err != nil { 215 return fmt.Errorf("error scaffolding container in the controller path (%s): %v", 216 controller.Path, err) 217 } 218 219 // Scaffold the command if informed 220 if len(s.command) > 0 { 221 // TODO: improve it to be an spec in the sample and api instead so that 222 // users can change the values 223 var res string 224 for _, value := range strings.Split(s.command, ",") { 225 res += fmt.Sprintf(" \"%s\",", strings.TrimSpace(value)) 226 } 227 // remove the latest , 228 res = res[:len(res)-1] 229 // remove the first space to not fail in the go fmt ./... 230 res = strings.TrimLeft(res, " ") 231 232 if err := util.InsertCode(controller.Path, `SecurityContext: &corev1.SecurityContext{ 233 RunAsNonRoot: &[]bool{true}[0], 234 AllowPrivilegeEscalation: &[]bool{false}[0], 235 Capabilities: &corev1.Capabilities{ 236 Drop: []corev1.Capability{ 237 "ALL", 238 }, 239 }, 240 },`, fmt.Sprintf(commandTemplate, res)); err != nil { 241 return fmt.Errorf("error scaffolding command in the controller path (%s): %v", 242 controller.Path, err) 243 } 244 } 245 246 // Scaffold the port if informed 247 if len(s.port) > 0 { 248 if err := util.InsertCode( 249 controller.Path, 250 `SecurityContext: &corev1.SecurityContext{ 251 RunAsNonRoot: &[]bool{true}[0], 252 AllowPrivilegeEscalation: &[]bool{false}[0], 253 Capabilities: &corev1.Capabilities{ 254 Drop: []corev1.Capability{ 255 "ALL", 256 }, 257 }, 258 },`, 259 fmt.Sprintf( 260 portTemplate, 261 strings.ToLower(s.resource.Kind), 262 strings.ToLower(s.resource.Kind)), 263 ); err != nil { 264 return fmt.Errorf("error scaffolding container port in the controller path (%s): %v", 265 controller.Path, 266 err) 267 } 268 } 269 270 if len(s.runAsUser) > 0 { 271 if err := util.InsertCode( 272 controller.Path, 273 `RunAsNonRoot: &[]bool{true}[0],`, 274 fmt.Sprintf(runAsUserTemplate, s.runAsUser), 275 ); err != nil { 276 return fmt.Errorf("error scaffolding user-id in the controller path (%s): %v", 277 controller.Path, err) 278 } 279 } 280 281 return nil 282 } 283 284 func (s *apiScaffolder) scaffoldCreateAPIFromKustomize(isLegacyLayout bool) error { 285 // Now we need call the kustomize/v1 plugin to do its scaffolds when we create a new API 286 // todo: when we have the go/v4 plugin we will also need to check what is the plugin used 287 // in the Project layout to know if we should use kustomize/v1 OR kustomize/v2-alpha 288 var kustomizeScaffolder plugins.Scaffolder 289 290 if isLegacyLayout { 291 kustomizeScaffolder = kustomizev1scaffolds.NewAPIScaffolder( 292 s.config, 293 s.resource, 294 true, 295 ) 296 } else { 297 kustomizeScaffolder = kustomizev2scaffolds.NewAPIScaffolder( 298 s.config, 299 s.resource, 300 true, 301 ) 302 } 303 304 kustomizeScaffolder.InjectFS(s.fs) 305 306 if err := kustomizeScaffolder.Scaffold(); err != nil { 307 return fmt.Errorf("error scaffolding kustomize files for the APIs: %v", err) 308 } 309 310 return nil 311 } 312 313 func (s *apiScaffolder) scaffoldCreateAPIFromGolang(isLegacyLayout bool) error { 314 // Now we need call the kustomize/v1 plugin to do its scaffolds when we create a new API 315 // todo: when we have the go/v4 plugin we will also need to check what is the plugin used 316 // in the Project layout to know if we should use kustomize/v1 OR kustomize/v2-alpha 317 if isLegacyLayout { 318 golangV3Scaffolder := golangv3scaffolds.NewAPIScaffolder(s.config, 319 s.resource, true) 320 golangV3Scaffolder.InjectFS(s.fs) 321 return golangV3Scaffolder.Scaffold() 322 } 323 golangV4Scaffolder := golangv4scaffolds.NewAPIScaffolder(s.config, 324 s.resource, true) 325 golangV4Scaffolder.InjectFS(s.fs) 326 return golangV4Scaffolder.Scaffold() 327 } 328 329 const containerTemplate = `Containers: []corev1.Container{{ 330 Image: image, 331 Name: "%s", 332 ImagePullPolicy: corev1.PullIfNotPresent, 333 // Ensure restrictive context for the container 334 // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 335 SecurityContext: &corev1.SecurityContext{ 336 RunAsNonRoot: &[]bool{true}[0], 337 AllowPrivilegeEscalation: &[]bool{false}[0], 338 Capabilities: &corev1.Capabilities{ 339 Drop: []corev1.Capability{ 340 "ALL", 341 }, 342 }, 343 }, 344 }}` 345 346 const runAsUserTemplate = ` 347 RunAsUser: &[]int64{%s}[0],` 348 349 const commandTemplate = ` 350 Command: []string{%s},` 351 352 const portTemplate = ` 353 Ports: []corev1.ContainerPort{{ 354 ContainerPort: %s.Spec.ContainerPort, 355 Name: "%s", 356 }},` 357 358 const recorderTemplate = ` 359 Recorder: mgr.GetEventRecorderFor("%s-controller"),` 360 361 const envVarTemplate = ` 362 - name: %s_IMAGE 363 value: %s`