mpdev/internal/resources/deployment_manager.go (184 lines of code) (raw):

// Copyright 2020 Google LLC // // 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 resources import ( "fmt" "os" "path/filepath" "strings" "github.com/GoogleCloudPlatform/marketplace-tools/mpdev/internal/util" "github.com/pkg/errors" "gopkg.in/yaml.v3" ) // DeploymentManagerAutogenTemplate generates a deployment manager template // given an autogen.yaml file. type DeploymentManagerAutogenTemplate struct { BaseResource Spec AutogenSpec outDir string } // AutogenSpec is defines the spec used for auto-generating deployment packages. type AutogenSpec struct { // Deployment Spec is documented in https://github.com/GoogleCloudPlatform/marketplace-tools/docs/autogen-reference.md DeploymentSpec map[string]interface{} `yaml:"deploymentSpec"` PackageInfo PackageInfo `yaml:"packageInfo"` } // PackageInfo describes the software packaged in a deployable solution. PackageInfo // is metadata displayed on the VM solution details page in the GCP marketplace // console. type PackageInfo struct { // Version of combined software components Version string // Name and version of OS OsInfo component `yaml:"osInfo"` // Names and versions of software components Components []component } type component struct { Name string Version string } type convertedSpec struct { PartnerID string `yaml:"partnerId"` SolutionID string `yaml:"solutionId"` Spec map[string]interface{} `yaml:"spec"` PartnerInfo map[string]interface{} `yaml:"partnerInfo"` SolutionInfo map[string]interface{} `yaml:"solutionInfo"` } // Apply generates a deployment manager template from an autogen file. func (dm *DeploymentManagerAutogenTemplate) Apply(registry Registry, dryRun bool) error { err := dm.validateSpec() if err != nil { return err } if dryRun { return nil } convertedSpec := dm.convertToAutogen() dir, err := util.CreateTmpDir("autogen") if err != nil { return err } dm.outDir = dir inputDir, err := util.CreateTmpDir("autogenInput") if err != nil { return err } defer os.RemoveAll(inputDir) inputFile, err := os.Create(filepath.Join(inputDir, "autogen.yaml")) if err != nil { return err } enc := yaml.NewEncoder(inputFile) err = enc.Encode(convertedSpec) if err != nil { return errors.Wrap(err, "failed to write autogen spec to temp file") } err = dm.runAutogen(registry, inputDir) return err } // Test is a no-op for autogen template resource. Tests are executed on the // DeploymentManagerTemplate resource. func (dm *DeploymentManagerAutogenTemplate) Test(registry Registry, dryRun bool) error { return nil } func (dm *DeploymentManagerAutogenTemplate) runAutogen(registry Registry, inputDir string) error { autogenImg := "gcr.io/cloud-marketplace-tools/dm/autogen" args := []string{"--input_type", "YAML", "--single_input", "/autogen/autogen.yaml", "--output_type", "PACKAGE", "--output", "/tmp/out"} cp := newContainerProcess( registry.GetExecutor(), autogenImg, args, []mount{ &bindMount{src: dm.outDir, dst: "/tmp/out"}, &bindMount{src: inputDir, dst: "/autogen"}, }, ) cmd := cp.getCommand() cmd.SetStderr(os.Stderr) cmd.SetStdout(os.Stdout) fmt.Printf("Executing autogen container: %s\n", autogenImg) err := cmd.Run() if err != nil { return errors.Wrap(err, "failed to execute autogen container with docker") } fmt.Printf("Wrote autogen output to directory: %s\n", dm.outDir) return nil } func (dm *DeploymentManagerAutogenTemplate) convertToAutogen() *convertedSpec { // Placeholder fields are either unused by autogen or overidden in the partner // portal UI when configuring solution details/metadata autogenSpec := &convertedSpec{ PartnerID: "placeholder", SolutionID: "solution", Spec: dm.Spec.DeploymentSpec, PartnerInfo: map[string]interface{}{"name": "placeholder"}, SolutionInfo: map[string]interface{}{"name": "placeholder", "version": dm.Spec.PackageInfo.Version}, } pkgGroups := []struct { Type string `yaml:",omitempty"` Components []component }{{ "SOFTWARE_GROUP_OS", []component{dm.Spec.PackageInfo.OsInfo}, }, { Components: dm.Spec.PackageInfo.Components, }} autogenSpec.SolutionInfo["packagedSoftwareGroups"] = pkgGroups return autogenSpec } func (dm *DeploymentManagerAutogenTemplate) validateSpec() error { packageInfo := dm.Spec.PackageInfo osInfo := packageInfo.OsInfo if osInfo.Version == "" || osInfo.Name == "" { return fmt.Errorf("osInfo version or name not specified. Ensure spec.packageInfo.osInfo in config file is set") } if len(packageInfo.Components) == 0 { return fmt.Errorf("no packageInfo Components. Ensure spec.packageInfo.Components in config file is set") } // Further deploymentSpec schema checks are done when executing autogen container. if len(dm.Spec.DeploymentSpec) == 0 { return fmt.Errorf("no deploymentSpec contents. Ensure spec.deploymentSpec in config file is set") } return nil } // DeploymentManagerTemplate saves a referenced Deployment Manager // template to GCS or the local filesystem type DeploymentManagerTemplate struct { BaseResource DeploymentManagerRef Reference // Uploads to gcs if file path prefixed with "gs://". Otherwise will // zip to given local file path. ZipFilePath string } // GetDependencies returns dependencies for DeploymentManagerTemplate func (dm *DeploymentManagerTemplate) GetDependencies() (r []Reference) { r = append(r, dm.DeploymentManagerRef) return r } // Apply uploads a Deployment Manager template to GCS. func (dm *DeploymentManagerTemplate) Apply(registry Registry, dryRun bool) error { dmRef := registry.GetResource(dm.DeploymentManagerRef) if dmRef == nil { return fmt.Errorf("autogen template not found %+v", dm.DeploymentManagerRef) } dmTemplate, ok := dmRef.(*DeploymentManagerAutogenTemplate) if !ok { return fmt.Errorf("referenced autogen template is not correct type %+v", dm.DeploymentManagerRef) } if dm.ZipFilePath == "" { return errors.New("ZipFilePath cannot be empty for DM template") } if dryRun { return nil } var localZipPath string isGCSUpload := strings.HasPrefix(dm.ZipFilePath, "gs://") if isGCSUpload { localZipPath = filepath.Join(dmTemplate.outDir, "dm_template.zip") } else { var err error localZipPath, err = registry.ResolveFilePath(dm, dm.ZipFilePath) if err != nil { return errors.Wrapf(err, "failed to resolve path to zipFile: %s", dm.ZipFilePath) } } executor := registry.GetExecutor() err := util.ZipDirectory(executor, localZipPath, dmTemplate.outDir) if err != nil { return errors.Wrapf(err, "failed to zip DM template to %s", localZipPath) } fmt.Printf("DM template zipped to %s\n", localZipPath) if isGCSUpload { cmd := executor.Command("gsutil", "cp", localZipPath, dm.ZipFilePath) cmd.SetStdout(os.Stdout) cmd.SetStderr(os.Stderr) fmt.Printf("Uploading DM template to GCS from:%s to:%s\n", localZipPath, dm.ZipFilePath) err = cmd.Run() if err != nil { return errors.Wrap(err, "failed to copy DM template to GCS") } fmt.Printf("Uploaded DM template to GCS path: %s\n", dm.ZipFilePath) } return nil } // Test executes Deployment Manager verification tasks such as creating // a deployment from the template and deleting a deployment func (dm *DeploymentManagerTemplate) Test(registry Registry, dryRun bool) error { return nil }