github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/live/init/cmdliveinit.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package init 16 17 import ( 18 "context" 19 "crypto/sha1" 20 goerrors "errors" 21 "fmt" 22 "os" 23 "path/filepath" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs" 29 "github.com/GoogleContainerTools/kpt/internal/errors" 30 "github.com/GoogleContainerTools/kpt/internal/pkg" 31 "github.com/GoogleContainerTools/kpt/internal/printer" 32 "github.com/GoogleContainerTools/kpt/internal/types" 33 "github.com/GoogleContainerTools/kpt/internal/util/attribution" 34 "github.com/GoogleContainerTools/kpt/internal/util/pathutil" 35 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 36 rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1" 37 "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" 38 "github.com/spf13/cobra" 39 "k8s.io/cli-runtime/pkg/genericclioptions" 40 k8scmdutil "k8s.io/kubectl/pkg/cmd/util" 41 "sigs.k8s.io/cli-utils/pkg/common" 42 "sigs.k8s.io/cli-utils/pkg/config" 43 "sigs.k8s.io/kustomize/kyaml/filesys" 44 "sigs.k8s.io/kustomize/kyaml/yaml" 45 ) 46 47 const defaultInventoryName = "inventory" 48 49 // InvExistsError defines new error when the inventory 50 // values have already been set on the Kptfile. 51 type InvExistsError struct{} 52 53 func (i *InvExistsError) Error() string { 54 return "inventory information already set for package" 55 } 56 57 // InvInRGExistsError defines new error when the inventory 58 // values have already been set on the ResourceGroup file and we will warn 59 // the user to migrate rather than init. This is part of kpt live STDIN work. 60 type InvInRGExistsError struct{} 61 62 func (i *InvInRGExistsError) Error() string { 63 return "inventory information already set for package" 64 } 65 66 // InvInKfExistsError defines new error when the inventory 67 // values have already been set on the Kptfile and we will warn 68 // the user to migrate rather than init. This is part of kpt live STDIN work. 69 type InvInKfExistsError struct{} 70 71 func (i *InvInKfExistsError) Error() string { 72 return "inventory information already set within Kptfile for package" 73 } 74 75 func NewRunner(ctx context.Context, factory k8scmdutil.Factory, 76 ioStreams genericclioptions.IOStreams) *Runner { 77 r := &Runner{ 78 ctx: ctx, 79 factory: factory, 80 ioStreams: ioStreams, 81 } 82 83 cmd := &cobra.Command{ 84 Use: "init [PKG_PATH]", 85 PreRunE: r.preRunE, 86 RunE: r.runE, 87 Short: livedocs.InitShort, 88 Long: livedocs.InitShort + "\n" + livedocs.InitLong, 89 Example: livedocs.InitExamples, 90 } 91 r.Command = cmd 92 93 cmd.Flags().StringVar(&r.Name, "name", "", "Inventory object name") 94 cmd.Flags().BoolVar(&r.Force, "force", false, "Set inventory values even if already set in Kptfile or ResourceGroup file") 95 cmd.Flags().BoolVar(&r.Quiet, "quiet", false, "If true, do not print output message for initialization") 96 cmd.Flags().StringVar(&r.InventoryID, "inventory-id", "", "Inventory id for the package") 97 cmd.Flags().StringVar(&r.RGFileName, "rg-file", rgfilev1alpha1.RGFileName, "Name of the file holding the ResourceGroup resource.") 98 return r 99 } 100 101 func NewCommand(ctx context.Context, f k8scmdutil.Factory, 102 ioStreams genericclioptions.IOStreams) *cobra.Command { 103 return NewRunner(ctx, f, ioStreams).Command 104 } 105 106 type Runner struct { 107 ctx context.Context 108 Command *cobra.Command 109 110 factory k8scmdutil.Factory 111 ioStreams genericclioptions.IOStreams 112 Force bool // Set inventory values even if already set in Kptfile 113 Name string // Inventory object name 114 RGFileName string // resourcegroup object filename 115 InventoryID string // Inventory object unique identifier label 116 Quiet bool // Output message during initialization 117 } 118 119 func (r *Runner) preRunE(_ *cobra.Command, _ []string) error { 120 dir := filepath.Dir(filepath.Clean(r.RGFileName)) 121 if dir != "." { 122 return fmt.Errorf("rg-file must be a valid filename") 123 } 124 return nil 125 } 126 127 func (r *Runner) runE(_ *cobra.Command, args []string) error { 128 const op errors.Op = "cmdliveinit.runE" 129 if len(args) == 0 { 130 // default to the current working directory 131 cwd, err := os.Getwd() 132 if err != nil { 133 return errors.E(op, err) 134 } 135 args = append(args, cwd) 136 } 137 138 dir, err := config.NormalizeDir(args[0]) 139 if err != nil { 140 return errors.E(op, err) 141 } 142 143 absPath, _, err := pathutil.ResolveAbsAndRelPaths(dir) 144 if err != nil { 145 return err 146 } 147 148 p, err := pkg.New(filesys.FileSystemOrOnDisk{}, absPath) 149 if err != nil { 150 return errors.E(op, err) 151 } 152 153 err = (&ConfigureInventoryInfo{ 154 Pkg: p, 155 Factory: r.factory, 156 Quiet: r.Quiet, 157 Name: r.Name, 158 InventoryID: r.InventoryID, 159 RGFileName: r.RGFileName, 160 Force: r.Force, 161 }).Run(r.ctx) 162 if err != nil { 163 return errors.E(op, p.UniquePath, err) 164 } 165 return nil 166 } 167 168 // ConfigureInventoryInfo contains the functionality for adding and updating 169 // the inventory information in the Kptfile. 170 type ConfigureInventoryInfo struct { 171 Pkg *pkg.Pkg 172 Factory k8scmdutil.Factory 173 Quiet bool 174 175 Name string 176 InventoryID string 177 RGFileName string 178 179 Force bool 180 } 181 182 // Run updates the inventory info in the package given by the Path. 183 func (c *ConfigureInventoryInfo) Run(ctx context.Context) error { 184 const op errors.Op = "cmdliveinit.Run" 185 pr := printer.FromContextOrDie(ctx) 186 187 namespace, err := config.FindNamespace(c.Factory.ToRawKubeConfigLoader(), c.Pkg.UniquePath.String()) 188 if err != nil { 189 return errors.E(op, c.Pkg.UniquePath, err) 190 } 191 namespace = strings.TrimSpace(namespace) 192 if !c.Quiet { 193 pr.Printf("initializing %q data (namespace: %s)...", c.RGFileName, namespace) 194 } 195 196 // Autogenerate the name if it is not provided through the flag. 197 if c.Name == "" { 198 randomSuffix := common.RandomStr() 199 c.Name = fmt.Sprintf("%s-%s", defaultInventoryName, randomSuffix) 200 } 201 202 // Autogenerate the inventory ID if not provided through the flag. 203 if c.InventoryID == "" { 204 c.InventoryID, err = generateID(namespace, c.Name, time.Now()) 205 if err != nil { 206 return errors.E(op, c.Pkg.UniquePath, err) 207 } 208 } 209 210 // Finally, create a ResourceGroup containing the inventory information. 211 err = createRGFile(c.Pkg, &kptfilev1.Inventory{ 212 Namespace: namespace, 213 Name: c.Name, 214 InventoryID: c.InventoryID, 215 }, c.RGFileName, c.Force) 216 if !c.Quiet { 217 if err == nil { 218 pr.Printf("success\n") 219 } else { 220 pr.Printf("failed\n") 221 } 222 } 223 if err != nil { 224 return errors.E(op, c.Pkg.UniquePath, err) 225 } 226 // add metrics annotation to package resources to track the usage as the resources 227 // will be applied using kpt live group 228 at := attribution.Attributor{PackagePaths: []string{c.Pkg.UniquePath.String()}, CmdGroup: "live"} 229 at.Process() 230 return nil 231 } 232 233 // createRGFile fills in the inventory object values into the resourcegroup object and writes to file storage. 234 func createRGFile(p *pkg.Pkg, inv *kptfilev1.Inventory, filename string, force bool) error { 235 const op errors.Op = "cmdliveinit.createRGFile" 236 // Read the resourcegroup object io io.dir 237 rg, err := p.ReadRGFile(filename) 238 if err != nil && !goerrors.Is(err, os.ErrNotExist) { 239 return errors.E(op, p.UniquePath, err) 240 } 241 242 // Read the Kptfile to ensure that inventory information is not in Kptfile either. 243 // Ignore error if Kptfile not found as we now support live init without a Kptfile since 244 // inventory information is stored in a ResourceGroup object. 245 kf, err := p.Kptfile() 246 if err != nil && !errors.Is(err, os.ErrNotExist) { 247 return errors.E(op, p.UniquePath, err) 248 } 249 // Validate the inventory values don't exist in Kptfile. 250 isEmpty := true 251 if kf != nil { 252 isEmpty = kptfileInventoryEmpty(kf.Inventory) 253 if !isEmpty && !force { 254 return errors.E(op, p.UniquePath, &InvInKfExistsError{}) 255 } 256 257 // Set the Kptfile inventory to be nil if we force write to resourcegroup instead. 258 kf.Inventory = nil 259 } 260 261 // Validate the inventory values don't already exist in Resourcegroup. 262 if rg != nil && !force { 263 return errors.E(op, p.UniquePath, &InvInRGExistsError{}) 264 } 265 // Initialize new resourcegroup object, as rg should have been nil. 266 rg = &rgfilev1alpha1.ResourceGroup{ResourceMeta: rgfilev1alpha1.DefaultMeta} 267 // // Finally, set the inventory parameters in the ResourceGroup object and write it. 268 rg.Name = inv.Name 269 rg.Namespace = inv.Namespace 270 rg.Labels = map[string]string{rgfilev1alpha1.RGInventoryIDLabel: inv.InventoryID} 271 if err := writeRGFile(p.UniquePath.String(), rg, filename); err != nil { 272 return errors.E(op, p.UniquePath, err) 273 } 274 275 // Rewrite Kptfile without inventory existing Kptfile contains inventory info. This 276 // is required when a user appends the force flag. 277 if !isEmpty { 278 if err := kptfileutil.WriteFile(p.UniquePath.String(), kf); err != nil { 279 return errors.E(op, p.UniquePath, err) 280 } 281 } 282 283 return nil 284 } 285 286 // writeRGFile writes a ResourceGroup inventory to local disk. 287 func writeRGFile(dir string, rg *rgfilev1alpha1.ResourceGroup, filename string) error { 288 const op errors.Op = "cmdliveinit.writeRGFile" 289 b, err := yaml.MarshalWithOptions(rg, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) 290 if err != nil { 291 return err 292 } 293 if _, err := os.Stat(filepath.Join(dir, filename)); err != nil && !goerrors.Is(err, os.ErrNotExist) { 294 return errors.E(op, errors.IO, types.UniquePath(dir), err) 295 } 296 297 // fyi: perm is ignored if the file already exists 298 err = os.WriteFile(filepath.Join(dir, filename), b, 0600) 299 if err != nil { 300 return errors.E(op, errors.IO, types.UniquePath(dir), err) 301 } 302 return nil 303 } 304 305 // generateID returns the string which is a SHA1 hash of the passed namespace 306 // and name, with the unix timestamp string concatenated. Returns an error 307 // if either the namespace or name are empty. 308 func generateID(namespace string, name string, t time.Time) (string, error) { 309 const op errors.Op = "cmdliveinit.generateID" 310 hashStr, err := generateHash(namespace, name) 311 if err != nil { 312 return "", errors.E(op, err) 313 } 314 timeStr := strconv.FormatInt(t.UTC().UnixNano(), 10) 315 return fmt.Sprintf("%s-%s", hashStr, timeStr), nil 316 } 317 318 // generateHash returns the SHA1 hash of the concatenated "namespace:name" string, 319 // or an error if either namespace or name is empty. 320 func generateHash(namespace string, name string) (string, error) { 321 const op errors.Op = "cmdliveinit.generateHash" 322 if len(namespace) == 0 || len(name) == 0 { 323 return "", errors.E(op, 324 fmt.Errorf("can not generate hash with empty namespace or name")) 325 } 326 str := fmt.Sprintf("%s:%s", namespace, name) 327 h := sha1.New() 328 if _, err := h.Write([]byte(str)); err != nil { 329 return "", errors.E(op, err) 330 } 331 return fmt.Sprintf("%x", (h.Sum(nil))), nil 332 } 333 334 // kptfileInventoryEmpty returns true if the Inventory structure 335 // in the Kptfile is empty; false otherwise. 336 func kptfileInventoryEmpty(inv *kptfilev1.Inventory) bool { 337 return inv == nil 338 }