mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
Feat: enhance addon init (#4370)
Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>
This commit is contained in:
+1
-1
@@ -80,7 +80,7 @@ import (
|
||||
|
||||
const (
|
||||
// ReadmeFileName is the addon readme file name
|
||||
ReadmeFileName string = "readme.md"
|
||||
ReadmeFileName string = "README.md"
|
||||
|
||||
// MetadataFileName is the addon meatadata.yaml file name
|
||||
MetadataFileName string = "metadata.yaml"
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela 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 addon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/format"
|
||||
"cuelang.org/go/encoding/gocode/gocodec"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
// CreateAddonFromHelmChart creates an addon scaffold from a Helm Chart, with a Helm component inside
|
||||
func CreateAddonFromHelmChart(addonName, addonPath, helmRepoURL, chartName, chartVersion string) error {
|
||||
if len(addonName) == 0 || len(helmRepoURL) == 0 || len(chartName) == 0 || len(chartVersion) == 0 {
|
||||
return fmt.Errorf("addon addonPath, helm URL, chart name, and chart verion should not be empty")
|
||||
}
|
||||
|
||||
// Currently, we do not check whether the Helm Chart actually exists, because it is just a scaffold.
|
||||
// The user can still edit it after creation.
|
||||
// Also, if the user is offline, we cannot check whether the Helm Chart exists.
|
||||
// TODO(charlie0129): check whether the Helm Chart exists (if the user wants)
|
||||
|
||||
// Make sure url is valid
|
||||
isValidURL := utils.IsValidURL(helmRepoURL)
|
||||
if !isValidURL {
|
||||
return fmt.Errorf("invalid helm repo url %s", helmRepoURL)
|
||||
}
|
||||
|
||||
err := preAddonCreation(addonName, addonPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create files like template.yaml, README.md, and etc.
|
||||
err = createFilesFromHelmChart(addonName, addonPath, helmRepoURL, chartName, chartVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create addon files: %w", err)
|
||||
}
|
||||
|
||||
postAddonCreation(addonPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAddonSample creates an empty addon scaffold, with some required files
|
||||
func CreateAddonSample(addonName, addonPath string) error {
|
||||
if len(addonName) == 0 || len(addonPath) == 0 {
|
||||
return fmt.Errorf("addon name and addon path should not be empty")
|
||||
}
|
||||
|
||||
err := preAddonCreation(addonName, addonPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = createSampleFiles(addonName, addonPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
postAddonCreation(addonPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// preAddonCreation is executed before creating an addon scaffold
|
||||
// It makes sure that user-provided info is valid.
|
||||
func preAddonCreation(addonName, addonPath string) error {
|
||||
if len(addonName) == 0 || len(addonPath) == 0 {
|
||||
return fmt.Errorf("addon name and addonPath should not be empty")
|
||||
}
|
||||
|
||||
// Make sure addon name is valid
|
||||
err := CheckAddonName(addonName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create dirs
|
||||
err = createAddonDirs(addonPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create addon structure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// postAddonCreation is after before creating an addon scaffold
|
||||
// It prints some instructions to get started.
|
||||
func postAddonCreation(addonPath string) {
|
||||
fmt.Println("Scaffold created in directory " +
|
||||
color.New(color.Bold).Sprint(addonPath) + ". What to do next:\n" +
|
||||
"- Check out our guide on how to build your own addon: " +
|
||||
color.BlueString("https://kubevela.io/docs/platform-engineers/addon/intro") + "\n" +
|
||||
"- Review and edit what we have generated in " + color.New(color.Bold).Sprint(addonPath) + "\n" +
|
||||
"- To enable the addon, run: " +
|
||||
color.New(color.FgGreen).Sprint("vela") + color.GreenString(" addon enable ") + color.New(color.Bold, color.FgGreen).Sprint(addonPath))
|
||||
}
|
||||
|
||||
// CheckAddonName checks if an addon name is valid
|
||||
func CheckAddonName(addonName string) error {
|
||||
if len(addonName) == 0 {
|
||||
return fmt.Errorf("addon name should not be empty")
|
||||
}
|
||||
|
||||
// Make sure addonName only contains lowercase letters, dashes, and numbers, e.g. some-addon
|
||||
re := regexp.MustCompile(`^[a-z\d]+(-[a-z\d]+)*$`)
|
||||
if !re.MatchString(addonName) {
|
||||
return fmt.Errorf("addon name should only cocntain lowercase letters, dashes, and numbers, e.g. some-addon")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createFilesFromHelmChart creates the file structure for a Helm Chart addon,
|
||||
// including template.yaml, readme.md, metadata.yaml, and <addon-nam>.cue.
|
||||
func createFilesFromHelmChart(addonName, addonPath, helmRepoURL, chartName, chartVersion string) error {
|
||||
// Generate template.yaml with an empty Application
|
||||
applicationTemplate := v1beta1.Application{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
Kind: "Application",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: addonName,
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
},
|
||||
}
|
||||
|
||||
applicationTemplateBytes, err := yaml.Marshal(applicationTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate metadata.yaml with `fluxcd` as a dependency because we are using helm.
|
||||
// However, this may change in the future, possibly with `argocd`.
|
||||
metadataTemplate := Meta{
|
||||
Name: addonName,
|
||||
Version: chartVersion,
|
||||
Description: "An addon for KubeVela.",
|
||||
Tags: []string{chartVersion},
|
||||
Dependencies: []*Dependency{{Name: "fluxcd"}},
|
||||
}
|
||||
metadataTemplateBytes, err := yaml.Marshal(metadataTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write template.yaml, readme.md, and metadata.yaml
|
||||
err = writeRequiredFiles(addonPath,
|
||||
applicationTemplateBytes,
|
||||
[]byte(strings.ReplaceAll(readmeTemplate, "ADDON_NAME", addonName)),
|
||||
metadataTemplateBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write addonName.cue, containing the helm chart
|
||||
addonResourcePath := path.Join(addonPath, ResourcesDirName, addonName+".cue")
|
||||
resourceTmpl := HelmCUETemplate{}
|
||||
resourceTmpl.Output.Type = "helm"
|
||||
resourceTmpl.Output.Properties.RepoType = "helm"
|
||||
resourceTmpl.Output.Properties.URL = helmRepoURL
|
||||
resourceTmpl.Output.Properties.Chart = chartName
|
||||
resourceTmpl.Output.Properties.Version = chartVersion
|
||||
err = writeHelmCUETemplate(resourceTmpl, addonResourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSampleFiles creates the file structure for an empty addon
|
||||
func createSampleFiles(addonName, addonPath string) error {
|
||||
// Generate metadata.yaml
|
||||
metadataTemplate := Meta{
|
||||
Name: addonName,
|
||||
Version: "1.0.0",
|
||||
Description: "An addon for KubeVela.",
|
||||
Tags: []string{},
|
||||
Dependencies: []*Dependency{},
|
||||
}
|
||||
metadataTemplateBytes, err := yaml.Marshal(metadataTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate template.yaml
|
||||
applicationTemplate := v1beta1.Application{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
Kind: "Application",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: addonName,
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
},
|
||||
}
|
||||
applicationTemplateBytes, err := yaml.Marshal(applicationTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writeRequiredFiles(addonPath,
|
||||
applicationTemplateBytes,
|
||||
[]byte(strings.ReplaceAll(readmeTemplate, "ADDON_NAME", addonName)),
|
||||
metadataTemplateBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeRequiredFiles creates required files for an addon,
|
||||
// including template.yaml, readme.md, and metadata.yaml
|
||||
func writeRequiredFiles(addonPath string, tmplContent, readmeContent, metadataContent []byte) error {
|
||||
// Write template.yaml
|
||||
templateFilePath := path.Join(addonPath, TemplateFileName)
|
||||
err := os.WriteFile(templateFilePath,
|
||||
tmplContent,
|
||||
0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write %s: %w", templateFilePath, err)
|
||||
}
|
||||
|
||||
// Write README.md
|
||||
readmeFilePath := path.Join(addonPath, ReadmeFileName)
|
||||
err = os.WriteFile(readmeFilePath,
|
||||
readmeContent,
|
||||
0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write %s: %w", readmeFilePath, err)
|
||||
}
|
||||
|
||||
// Write metadata.yaml
|
||||
metadataFilePath := path.Join(addonPath, MetadataFileName)
|
||||
err = os.WriteFile(metadataFilePath,
|
||||
metadataContent,
|
||||
0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write %s: %w", metadataFilePath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createAddonDirs creates the directory structure for an addon
|
||||
func createAddonDirs(addonDir string) error {
|
||||
// Make sure addonDir is pointing to an empty directory, or does not exist at all
|
||||
// so that we can create it later
|
||||
_, err := os.Stat(addonDir)
|
||||
if !os.IsNotExist(err) {
|
||||
emptyDir, err := utils.IsEmptyDir(addonDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("we can't create directory %s. Make sure the name has not already been taken and you have the proper rights to write to it", addonDir)
|
||||
}
|
||||
|
||||
if !emptyDir {
|
||||
return fmt.Errorf("directory %s is not empty. To avoid any data loss, please manually delete it first, then try again", addonDir)
|
||||
}
|
||||
|
||||
// Now we are sure addonPath is en empty dir, delete it
|
||||
err = os.Remove(addonDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
err = os.MkdirAll(addonDir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dirs := []string{
|
||||
path.Join(addonDir, ResourcesDirName),
|
||||
path.Join(addonDir, DefinitionsDirName),
|
||||
path.Join(addonDir, DefSchemaName),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
// nolint:gosec
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeHelmCUETemplate writes a cue, with a helm component inside, intended as addon resource
|
||||
func writeHelmCUETemplate(tmpl HelmCUETemplate, filePath string) error {
|
||||
r := cue.Runtime{}
|
||||
v, err := gocodec.New(&r, nil).Decode(tmpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use `output` value
|
||||
v = v.Lookup("output")
|
||||
// Format output
|
||||
bs, err := format.Node(v.Syntax())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Append "output: " to the beginning of the string, like "output: {}"
|
||||
bs = append([]byte("output: "), bs...)
|
||||
err = os.WriteFile(filePath, bs, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HelmCUETemplate is a template for a helm component .cue in an addon
|
||||
type HelmCUETemplate struct {
|
||||
Output struct {
|
||||
Type string `json:"type"`
|
||||
Properties struct {
|
||||
RepoType string `json:"repoType"`
|
||||
URL string `json:"url"`
|
||||
Chart string `json:"chart"`
|
||||
Version string `json:"version"`
|
||||
} `json:"properties"`
|
||||
} `json:"output"`
|
||||
}
|
||||
|
||||
const (
|
||||
readmeTemplate = "# ADDON_NAME\n" +
|
||||
"\n" +
|
||||
"This is an addon template. Check how to build your own addon: https://kubevela.net/docs/platform-engineers/addon/intro\n" +
|
||||
"\n" +
|
||||
"## Directory Structure\n" +
|
||||
"\n" +
|
||||
"- `template.yaml`: contains the basic app, you can add some component and workflow to meet your requirements. Other files in `resources/` and `definitions/` will be rendered as Components and appended in `spec.components`\n" +
|
||||
"- `metadata.yaml`: contains addon metadata information.\n" +
|
||||
"- `definitions/`: contains the X-Definition yaml/cue files. These file will be rendered as KubeVela Component in `template.yaml`\n" +
|
||||
"- `resources/`:\n" +
|
||||
" - `parameter.cue` to expose parameters. It will be converted to JSON schema and rendered in UI forms.\n" +
|
||||
" - All other files will be rendered as KubeVela Components. It can be one of the two types:\n" +
|
||||
" - YAML file that contains only one resource. This will be rendered as a `raw` component\n" +
|
||||
" - CUE template file that can read user input as `parameter.XXX` as defined `parameter.cue`.\n" +
|
||||
" Basically the CUE template file will be combined with `parameter.cue` to render a resource.\n" +
|
||||
" **You can specify the type and trait in this format**\n" +
|
||||
""
|
||||
)
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela 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 addon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestCheckAddonName(t *testing.T) {
|
||||
var err error
|
||||
|
||||
err = CheckAddonName("")
|
||||
assert.ErrorContains(t, err, "should not be empty")
|
||||
|
||||
invalidNames := []string{
|
||||
"-addon",
|
||||
"addon-",
|
||||
"Caps",
|
||||
"=",
|
||||
".",
|
||||
}
|
||||
for _, name := range invalidNames {
|
||||
err = CheckAddonName(name)
|
||||
assert.ErrorContains(t, err, "should only")
|
||||
}
|
||||
|
||||
validNames := []string{
|
||||
"addon-name",
|
||||
"3-addon-name",
|
||||
"addon-name-3",
|
||||
"addon",
|
||||
}
|
||||
for _, name := range validNames {
|
||||
err = CheckAddonName(name)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteHelmCUETemplate(t *testing.T) {
|
||||
resourceTmpl := HelmCUETemplate{}
|
||||
resourceTmpl.Output.Type = "helm"
|
||||
resourceTmpl.Output.Properties.RepoType = "helm"
|
||||
resourceTmpl.Output.Properties.URL = "https://charts.bitnami.com/bitnami"
|
||||
resourceTmpl.Output.Properties.Chart = "bitnami/nginx"
|
||||
resourceTmpl.Output.Properties.Version = "12.0.4"
|
||||
err := writeHelmCUETemplate(resourceTmpl, "test.cue")
|
||||
assert.NilError(t, err)
|
||||
defer func() {
|
||||
_ = os.Remove("test.cue")
|
||||
}()
|
||||
data, err := os.ReadFile("test.cue")
|
||||
assert.NilError(t, err)
|
||||
expected := `output: {
|
||||
type: "helm"
|
||||
properties: {
|
||||
url: "https://charts.bitnami.com/bitnami"
|
||||
repoType: "helm"
|
||||
chart: "bitnami/nginx"
|
||||
version: "12.0.4"
|
||||
}
|
||||
}`
|
||||
assert.Equal(t, string(data), expected)
|
||||
}
|
||||
|
||||
func TestCreateAddonFromHelmChart(t *testing.T) {
|
||||
err := CreateAddonFromHelmChart("", "", "", "bitnami/nginx", "12.0.4")
|
||||
assert.ErrorContains(t, err, "should not be empty")
|
||||
|
||||
checkFiles := func(base string) {
|
||||
fileList := []string{
|
||||
"definitions",
|
||||
path.Join("resources", base+".cue"),
|
||||
"schemas",
|
||||
MetadataFileName,
|
||||
ReadmeFileName,
|
||||
TemplateFileName,
|
||||
}
|
||||
for _, file := range fileList {
|
||||
_, err = os.Stat(path.Join(base, file))
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty dir already exists
|
||||
_ = os.MkdirAll("test-addon", 0755)
|
||||
err = CreateAddonFromHelmChart("test-addon", "./test-addon", "https://charts.bitnami.com/bitnami", "bitnami/nginx", "12.0.4")
|
||||
checkFiles("test-addon")
|
||||
defer func() {
|
||||
_ = os.RemoveAll("test-addon")
|
||||
}()
|
||||
|
||||
// Non-empty dir already exists
|
||||
err = CreateAddonFromHelmChart("test-addon", "test-addon", "https://charts.bitnami.com/bitnami", "bitnami/nginx", "12.0.4")
|
||||
assert.ErrorContains(t, err, "not empty")
|
||||
|
||||
// Name already taken
|
||||
err = os.WriteFile("already-taken", []byte{}, 0644)
|
||||
assert.NilError(t, err)
|
||||
defer func() {
|
||||
_ = os.Remove("already-taken")
|
||||
}()
|
||||
err = CreateAddonFromHelmChart("already-taken", "already-taken", "https://charts.bitnami.com/bitnami", "bitnami/nginx", "12.0.4")
|
||||
assert.ErrorContains(t, err, "can't create")
|
||||
|
||||
// Invalid addon name
|
||||
err = CreateAddonFromHelmChart("/", "./a", "https://charts.bitnami.com/bitnami", "bitnami/nginx", "12.0.4")
|
||||
assert.ErrorContains(t, err, "should only")
|
||||
|
||||
// Invalid URL
|
||||
err = CreateAddonFromHelmChart("invalid-url", "invalid-url", "invalid-url", "bitnami/nginx", "12.0.4")
|
||||
assert.ErrorContains(t, err, "invalid helm repo url")
|
||||
}
|
||||
|
||||
func TestCreateAddonSample(t *testing.T) {
|
||||
checkFiles := func(base string) {
|
||||
fileList := []string{
|
||||
"definitions",
|
||||
"resources",
|
||||
"schemas",
|
||||
MetadataFileName,
|
||||
ReadmeFileName,
|
||||
TemplateFileName,
|
||||
}
|
||||
for _, file := range fileList {
|
||||
_, err := os.Stat(path.Join(base, file))
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Normal creation
|
||||
err := CreateAddonSample("test-addon", "test-addon")
|
||||
assert.NilError(t, err)
|
||||
checkFiles("test-addon")
|
||||
|
||||
// Non-empty dir already exists
|
||||
err = CreateAddonSample("test-addon", "test-addon")
|
||||
assert.ErrorContains(t, err, "directory")
|
||||
|
||||
defer func() {
|
||||
_ = os.RemoveAll("test-addon")
|
||||
}()
|
||||
|
||||
err = CreateAddonSample("", "")
|
||||
assert.ErrorContains(t, err, "empty")
|
||||
}
|
||||
|
||||
func TestPreAddonCreation(t *testing.T) {
|
||||
err := preAddonCreation("", "")
|
||||
assert.ErrorContains(t, err, "empty")
|
||||
|
||||
err = preAddonCreation("=", "a")
|
||||
assert.ErrorContains(t, err, "name")
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela 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 addon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/format"
|
||||
"cuelang.org/go/encoding/gocode/gocodec"
|
||||
"github.com/fatih/color"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// AddonNameRegex is the regex to validate addon names
|
||||
AddonNameRegex = `^[a-z\d]+(-[a-z\d]+)*$`
|
||||
helmComponentDependency = "fluxcd"
|
||||
)
|
||||
|
||||
// InitCmd contains the options to initialize an addon scaffold
|
||||
type InitCmd struct {
|
||||
AddonName string
|
||||
NoSamples bool
|
||||
HelmRepoURL string
|
||||
HelmChartName string
|
||||
HelmChartVersion string
|
||||
Path string
|
||||
Overwrite bool
|
||||
RefObjURLs []string
|
||||
AppTmpl v1beta1.Application
|
||||
Metadata Meta
|
||||
Readme string
|
||||
Resources []ElementFile
|
||||
Schemas []ElementFile
|
||||
Views []ElementFile
|
||||
Definitions []ElementFile
|
||||
}
|
||||
|
||||
// CreateScaffold creates an addon scaffold
|
||||
func (cmd *InitCmd) CreateScaffold() error {
|
||||
var err error
|
||||
|
||||
if len(cmd.AddonName) == 0 || len(cmd.Path) == 0 {
|
||||
return fmt.Errorf("addon name and path should not be empty")
|
||||
}
|
||||
|
||||
err = CheckAddonName(cmd.AddonName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cmd.createDirs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create addon structure: %w", err)
|
||||
}
|
||||
// Delete created files if an error occurred afterwards.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = os.RemoveAll(cmd.Path)
|
||||
}
|
||||
}()
|
||||
|
||||
cmd.createRequiredFiles()
|
||||
|
||||
if cmd.HelmChartName != "" && cmd.HelmChartVersion != "" && cmd.HelmRepoURL != "" {
|
||||
klog.Info("Creating Helm component...")
|
||||
err = cmd.createHelmComponent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(cmd.RefObjURLs) > 0 {
|
||||
klog.Info("Creating ref-objects URL component...")
|
||||
err = cmd.createURLComponent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !cmd.NoSamples {
|
||||
cmd.createSamples()
|
||||
}
|
||||
|
||||
err = cmd.writeFiles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print some instructions to get started.
|
||||
fmt.Println("\nScaffold created in directory " +
|
||||
color.New(color.Bold).Sprint(cmd.Path) + ". What to do next:\n" +
|
||||
"- Check out our guide on how to build your own addon: " +
|
||||
color.New(color.Bold, color.FgBlue).Sprint("https://kubevela.io/docs/platform-engineers/addon/intro") + "\n" +
|
||||
"- Review and edit what we have generated in " + color.New(color.Bold).Sprint(cmd.Path) + "\n" +
|
||||
"- To enable this addon, run: " +
|
||||
color.New(color.FgGreen).Sprint("vela") + color.GreenString(" addon enable ") + color.New(color.Bold, color.FgGreen).Sprint(cmd.Path))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckAddonName checks if an addon name is valid
|
||||
func CheckAddonName(addonName string) error {
|
||||
if len(addonName) == 0 {
|
||||
return fmt.Errorf("addon name should not be empty")
|
||||
}
|
||||
|
||||
// Make sure addonName only contains lowercase letters, dashes, and numbers, e.g. some-addon
|
||||
re := regexp.MustCompile(AddonNameRegex)
|
||||
if !re.MatchString(addonName) {
|
||||
return fmt.Errorf("addon name should only cocntain lowercase letters, dashes, and numbers, e.g. some-addon")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSamples creates sample files
|
||||
func (cmd *InitCmd) createSamples() {
|
||||
// Sample Definition mytrait.cue
|
||||
cmd.Definitions = append(cmd.Definitions, ElementFile{
|
||||
Data: traitTemplate,
|
||||
Name: "mytrait.cue",
|
||||
})
|
||||
// Sample Resource
|
||||
cmd.Resources = append(cmd.Resources, ElementFile{
|
||||
Data: resourceTemplate,
|
||||
Name: "myresource.cue",
|
||||
}, ElementFile{
|
||||
Data: parameterTemplate,
|
||||
Name: "parameter.cue",
|
||||
})
|
||||
// Sample schema
|
||||
cmd.Schemas = append(cmd.Schemas, ElementFile{
|
||||
Data: schemaTemplate,
|
||||
Name: "myschema.yaml",
|
||||
})
|
||||
// Sample View
|
||||
cmd.Views = append(cmd.Views, ElementFile{
|
||||
Data: strings.ReplaceAll(viewTemplate, "ADDON_NAME", cmd.AddonName),
|
||||
Name: "my-view.cue",
|
||||
})
|
||||
}
|
||||
|
||||
// createRequiredFiles creates README.md, template.yaml and metadata.yaml
|
||||
func (cmd *InitCmd) createRequiredFiles() {
|
||||
// README.md
|
||||
cmd.Readme = strings.ReplaceAll(readmeTemplate, "ADDON_NAME", cmd.AddonName)
|
||||
|
||||
// template.yaml
|
||||
cmd.AppTmpl = v1beta1.Application{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
Kind: "Application",
|
||||
},
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: cmd.AddonName,
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
// Prevent nulls after serialization
|
||||
Components: []common.ApplicationComponent{},
|
||||
},
|
||||
}
|
||||
|
||||
// metadata.yaml
|
||||
cmd.Metadata = Meta{
|
||||
Name: cmd.AddonName,
|
||||
Version: "1.0.0",
|
||||
Description: "An addon for KubeVela.",
|
||||
Tags: []string{"my-tag"},
|
||||
Dependencies: []*Dependency{},
|
||||
DeployTo: &DeployTo{
|
||||
RuntimeCluster: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createHelmComponent creates a <addon-name-helm>.cue in /resources
|
||||
func (cmd *InitCmd) createHelmComponent() error {
|
||||
// Make fluxcd a dependency, since it uses a helm component
|
||||
cmd.Metadata.addDependency(helmComponentDependency)
|
||||
// Make addon version same as chart version
|
||||
cmd.Metadata.Version = cmd.HelmChartVersion
|
||||
|
||||
// Create a <addon-name-helm>.cue in resources
|
||||
tmpl := helmComponentTmpl{}
|
||||
tmpl.Type = "helm"
|
||||
tmpl.Properties.RepoType = "helm"
|
||||
tmpl.Properties.URL = cmd.HelmRepoURL
|
||||
tmpl.Properties.Chart = cmd.HelmChartName
|
||||
tmpl.Properties.Version = cmd.HelmChartVersion
|
||||
|
||||
str, err := toCUEResourceString(tmpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Resources = append(cmd.Resources, ElementFile{
|
||||
Name: "helm.cue",
|
||||
Data: str,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createURLComponent creates a ref-object component containing URLs
|
||||
func (cmd *InitCmd) createURLComponent() error {
|
||||
tmpl := refObjURLTmpl{Type: "ref-objects"}
|
||||
|
||||
for _, url := range cmd.RefObjURLs {
|
||||
if !utils.IsValidURL(url) {
|
||||
return fmt.Errorf("%s is not a valid url", url)
|
||||
}
|
||||
|
||||
tmpl.Properties.URLs = append(tmpl.Properties.URLs, url)
|
||||
}
|
||||
|
||||
str, err := toCUEResourceString(tmpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Resources = append(cmd.Resources, ElementFile{
|
||||
Data: str,
|
||||
Name: "from-url.cue",
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// toCUEResourceString formats object to CUE string used in addons
|
||||
func toCUEResourceString(obj interface{}) (string, error) {
|
||||
r := cue.Runtime{}
|
||||
v, err := gocodec.New(&r, nil).Decode(obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bs, err := format.Node(v.Syntax())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Append "output: " to the beginning of the string, like "output: {}"
|
||||
bs = append([]byte("output: "), bs...)
|
||||
|
||||
return string(bs), nil
|
||||
}
|
||||
|
||||
// addDependency adds a dependency into metadata.yaml
|
||||
func (m *Meta) addDependency(dep string) {
|
||||
for _, d := range m.Dependencies {
|
||||
if d.Name == dep {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
m.Dependencies = append(m.Dependencies, &Dependency{Name: dep})
|
||||
}
|
||||
|
||||
// createDirs creates the directory structure for an addon
|
||||
func (cmd *InitCmd) createDirs() error {
|
||||
// Make sure addonDir is pointing to an empty directory, or does not exist at all
|
||||
// so that we can create it later
|
||||
_, err := os.Stat(cmd.Path)
|
||||
if !os.IsNotExist(err) {
|
||||
emptyDir, err := utils.IsEmptyDir(cmd.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("we can't create directory %s. Make sure the name has not already been taken and you have the proper rights to write to it", cmd.Path)
|
||||
}
|
||||
|
||||
if !emptyDir {
|
||||
if !cmd.Overwrite {
|
||||
return fmt.Errorf("directory %s is not empty. To avoid any data loss, please manually delete it first or use -f, then try again", cmd.Path)
|
||||
}
|
||||
klog.Warningf("Overwriting non-empty directory %s", cmd.Path)
|
||||
}
|
||||
|
||||
// Now we are sure addonPath is en empty dir, (or the user want to overwrite), delete it
|
||||
err = os.RemoveAll(cmd.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
err = os.MkdirAll(cmd.Path, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dirs := []string{
|
||||
path.Join(cmd.Path, ResourcesDirName),
|
||||
path.Join(cmd.Path, DefinitionsDirName),
|
||||
path.Join(cmd.Path, DefSchemaName),
|
||||
path.Join(cmd.Path, ViewDirName),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
// nolint:gosec
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFiles writes addon to disk
|
||||
func (cmd *InitCmd) writeFiles() error {
|
||||
var files []ElementFile
|
||||
|
||||
files = append(files, ElementFile{
|
||||
Name: ReadmeFileName,
|
||||
Data: cmd.Readme,
|
||||
})
|
||||
|
||||
for _, v := range cmd.Resources {
|
||||
files = append(files, ElementFile{
|
||||
Data: v.Data,
|
||||
Name: filepath.Join(ResourcesDirName, v.Name),
|
||||
})
|
||||
}
|
||||
for _, v := range cmd.Views {
|
||||
files = append(files, ElementFile{
|
||||
Data: v.Data,
|
||||
Name: filepath.Join(ViewDirName, v.Name),
|
||||
})
|
||||
}
|
||||
for _, v := range cmd.Definitions {
|
||||
files = append(files, ElementFile{
|
||||
Data: v.Data,
|
||||
Name: filepath.Join(DefinitionsDirName, v.Name),
|
||||
})
|
||||
}
|
||||
for _, v := range cmd.Schemas {
|
||||
files = append(files, ElementFile{
|
||||
Data: v.Data,
|
||||
Name: filepath.Join(DefSchemaName, v.Name),
|
||||
})
|
||||
}
|
||||
|
||||
// Prepare template.yaml
|
||||
tmplBytes, err := yaml.Marshal(cmd.AppTmpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, ElementFile{
|
||||
Data: string(tmplBytes),
|
||||
Name: TemplateFileName,
|
||||
})
|
||||
|
||||
// Prepare metadata.yaml
|
||||
metaBytes, err := yaml.Marshal(cmd.Metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, ElementFile{
|
||||
Data: string(metaBytes),
|
||||
Name: MetadataFileName,
|
||||
})
|
||||
|
||||
// Write files
|
||||
for _, f := range files {
|
||||
err := os.WriteFile(filepath.Join(cmd.Path, f.Name), []byte(f.Data), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// helmComponentTmpl is a template for a helm component .cue in an addon
|
||||
type helmComponentTmpl struct {
|
||||
Type string `json:"type"`
|
||||
Properties struct {
|
||||
RepoType string `json:"repoType"`
|
||||
URL string `json:"url"`
|
||||
Chart string `json:"chart"`
|
||||
Version string `json:"version"`
|
||||
} `json:"properties"`
|
||||
}
|
||||
|
||||
// refObjURLTmpl is a template for ref-objects containing URLs in an addon
|
||||
type refObjURLTmpl struct {
|
||||
Type string `json:"type"`
|
||||
Properties struct {
|
||||
URLs []string `json:"urls"`
|
||||
} `json:"properties"`
|
||||
}
|
||||
|
||||
const (
|
||||
readmeTemplate = "# ADDON_NAME\n" +
|
||||
"\n" +
|
||||
"This is an addon template. Check how to build your own addon: https://kubevela.net/docs/platform-engineers/addon/intro\n" +
|
||||
""
|
||||
viewTemplate = `// We put VelaQL views in views directory.
|
||||
//
|
||||
// VelaQL(Vela Query Language) is a resource query language for KubeVela,
|
||||
// used to query status of any extended resources in application-level.
|
||||
// Reference: https://kubevela.net/docs/platform-engineers/system-operation/velaql
|
||||
//
|
||||
// This VelaQL View querys the status of this addon.
|
||||
// Use this view to query by:
|
||||
// vela ql --query 'my-view{addonName:ADDON_NAME}.status'
|
||||
// You should see 'running'.
|
||||
|
||||
import (
|
||||
"vela/ql"
|
||||
)
|
||||
|
||||
app: ql.#Read & {
|
||||
value: {
|
||||
kind: "Application"
|
||||
apiVersion: "core.oam.dev/v1beta1"
|
||||
metadata: {
|
||||
name: "addon-" + parameter.addonName
|
||||
namespace: "vela-system"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
addonName: *"ADDON_NAME" | string
|
||||
}
|
||||
|
||||
status: app.value.status.status
|
||||
`
|
||||
traitTemplate = `// We put Definitions in definitions directory.
|
||||
// References:
|
||||
// - https://kubevela.net/docs/platform-engineers/cue/definition-edit
|
||||
// - https://kubevela.net/docs/platform-engineers/addon/intro#definitions-directoryoptional
|
||||
"mytrait": {
|
||||
alias: "mt"
|
||||
annotations: {}
|
||||
attributes: {
|
||||
appliesToWorkloads: [
|
||||
"deployments.apps",
|
||||
"replicasets.apps",
|
||||
"statefulsets.apps",
|
||||
]
|
||||
conflictsWith: []
|
||||
podDisruptive: false
|
||||
workloadRefPath: ""
|
||||
}
|
||||
description: "My trait description."
|
||||
labels: {}
|
||||
type: "trait"
|
||||
}
|
||||
template: {
|
||||
parameter: {param: ""}
|
||||
outputs: {sample: {}}
|
||||
}
|
||||
`
|
||||
resourceTemplate = `// We put Components in resources directory.
|
||||
// References:
|
||||
// - https://kubevela.net/docs/end-user/components/references
|
||||
// - https://kubevela.net/docs/platform-engineers/addon/intro#resources-directoryoptional
|
||||
output: {
|
||||
type: "k8s-objects"
|
||||
properties: {
|
||||
objects: [
|
||||
{
|
||||
// This creates a plain old Kubernetes namespace
|
||||
apiVersion: "v1"
|
||||
kind: "Namespace"
|
||||
// We can use the parameter defined in parameter.cue like this.
|
||||
metadata: name: parameter.myparam
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
parameterTemplate = `// parameter.cue is used to store addon parameters.
|
||||
//
|
||||
// You can use these parameters in other resources by 'parameter.myparam'
|
||||
//
|
||||
// For example, you can use parameters to allow the user to customize
|
||||
// container images, ports, and etc.
|
||||
parameter: {
|
||||
// +usage=Custom parameter description
|
||||
myparam: *"myns" | string
|
||||
}
|
||||
`
|
||||
schemaTemplate = `# We put UI Schemas that correspond to Definitions in schemas directory.
|
||||
# References:
|
||||
# - https://kubevela.net/docs/platform-engineers/addon/intro#schemas-directoryoptional
|
||||
# - https://kubevela.net/docs/reference/ui-schema
|
||||
- jsonKey: myparam
|
||||
label: MyParam
|
||||
validate:
|
||||
required: true
|
||||
`
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela 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 addon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestCheckAddonName(t *testing.T) {
|
||||
var err error
|
||||
|
||||
err = CheckAddonName("")
|
||||
assert.ErrorContains(t, err, "should not be empty")
|
||||
|
||||
invalidNames := []string{
|
||||
"-addon",
|
||||
"addon-",
|
||||
"Caps",
|
||||
"=",
|
||||
".",
|
||||
}
|
||||
for _, name := range invalidNames {
|
||||
err = CheckAddonName(name)
|
||||
assert.ErrorContains(t, err, "should only")
|
||||
}
|
||||
|
||||
validNames := []string{
|
||||
"addon-name",
|
||||
"3-addon-name",
|
||||
"addon-name-3",
|
||||
"addon",
|
||||
}
|
||||
for _, name := range validNames {
|
||||
err = CheckAddonName(name)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCmd_CreateScaffold(t *testing.T) {
|
||||
var err error
|
||||
|
||||
// empty addon name or path
|
||||
cmd := InitCmd{}
|
||||
err = cmd.CreateScaffold()
|
||||
assert.ErrorContains(t, err, "be empty")
|
||||
|
||||
// invalid addon name
|
||||
cmd = InitCmd{
|
||||
AddonName: "-name",
|
||||
Path: "name",
|
||||
}
|
||||
err = cmd.CreateScaffold()
|
||||
assert.ErrorContains(t, err, "should only")
|
||||
|
||||
// dir already exists
|
||||
cmd = InitCmd{
|
||||
AddonName: "name",
|
||||
Path: "testdata",
|
||||
}
|
||||
err = cmd.CreateScaffold()
|
||||
assert.ErrorContains(t, err, "cannot create")
|
||||
|
||||
// with helm component
|
||||
cmd = InitCmd{
|
||||
AddonName: "with-helm",
|
||||
Path: "with-helm",
|
||||
HelmRepoURL: "https://charts.bitnami.com/bitnami",
|
||||
HelmChartVersion: "12.0.0",
|
||||
HelmChartName: "nginx",
|
||||
}
|
||||
err = cmd.CreateScaffold()
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll("with-helm")
|
||||
_, err = os.Stat(filepath.Join("with-helm", ResourcesDirName, "helm.cue"))
|
||||
assert.NilError(t, err)
|
||||
|
||||
// with ref-obj
|
||||
cmd = InitCmd{
|
||||
AddonName: "with-refobj",
|
||||
Path: "with-refobj",
|
||||
RefObjURLs: []string{"https:"},
|
||||
}
|
||||
err = cmd.CreateScaffold()
|
||||
assert.ErrorContains(t, err, "not a valid url")
|
||||
cmd.RefObjURLs[0] = "https://some.com"
|
||||
err = cmd.CreateScaffold()
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll("with-refobj")
|
||||
_, err = os.Stat(filepath.Join("with-refobj", ResourcesDirName, "from-url.cue"))
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
+1
-1
@@ -92,7 +92,7 @@ type Meta struct {
|
||||
// DeployTo defines where the addon to deploy to
|
||||
type DeployTo struct {
|
||||
// This field keep the compatible for older case
|
||||
LegacyRuntimeCluster bool `json:"runtime_cluster"`
|
||||
LegacyRuntimeCluster bool `json:"runtime_cluster,omitempty"`
|
||||
DisableControlPlane bool `json:"disableControlPlane"`
|
||||
RuntimeCluster bool `json:"runtimeCluster"`
|
||||
}
|
||||
|
||||
+10
-2
@@ -234,7 +234,11 @@ func TestIsAddonDir(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "missing")
|
||||
|
||||
// Pass all checks
|
||||
err = CreateAddonSample("testaddon2", filepath.Join("testdata", "testaddon2"))
|
||||
cmd := InitCmd{
|
||||
Path: filepath.Join("testdata", "testaddon2"),
|
||||
AddonName: "testaddon2",
|
||||
}
|
||||
err = cmd.CreateScaffold()
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(filepath.Join("testdata", "testaddon2"))
|
||||
@@ -252,7 +256,11 @@ func TestMakeChart(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "not an addon dir")
|
||||
|
||||
// Valid addon dir
|
||||
err = CreateAddonSample("testaddon2", filepath.Join("testdata", "testaddon"))
|
||||
cmd := InitCmd{
|
||||
Path: filepath.Join("testdata", "testaddon"),
|
||||
AddonName: "testaddon",
|
||||
}
|
||||
err = cmd.CreateScaffold()
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(filepath.Join("testdata", "testaddon"))
|
||||
|
||||
+42
-30
@@ -96,7 +96,7 @@ func NewAddonCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *
|
||||
NewAddonRegistryCommand(c, ioStreams),
|
||||
NewAddonUpgradeCommand(c, ioStreams),
|
||||
NewAddonPackageCommand(c),
|
||||
NewAddonCreateCommand(),
|
||||
NewAddonInitCommand(),
|
||||
NewAddonPushCommand(c),
|
||||
)
|
||||
return cmd
|
||||
@@ -376,33 +376,35 @@ func NewAddonStatusCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Com
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewAddonCreateCommand creates an addon scaffold
|
||||
func NewAddonCreateCommand() *cobra.Command {
|
||||
var helmRepoURL string
|
||||
var chartName string
|
||||
var chartVersion string
|
||||
var path string
|
||||
// NewAddonInitCommand creates an addon scaffold
|
||||
func NewAddonInitCommand() *cobra.Command {
|
||||
var (
|
||||
helmRepoURL string
|
||||
chartName string
|
||||
chartVersion string
|
||||
urls []string
|
||||
path string
|
||||
noSample bool
|
||||
overwrite bool
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "create an addon scaffold",
|
||||
Long: "Create an addon scaffold for quick starting. A Helm Component is generated if you provide Chart-related parameters.",
|
||||
Example: ` vela addon init mongodb --helm-repo-url=https://marketplace.azurecr.io/helm/v1/repo --chart=mongodb --version=12.1.16
|
||||
will create something like this:
|
||||
mongodb/
|
||||
├── definitions
|
||||
├── metadata.yaml
|
||||
├── readme.md
|
||||
├── resources
|
||||
│ └── mongodb.cue
|
||||
├── schemas
|
||||
└── template.yaml
|
||||
Long: "Create an addon scaffold for quick starting.",
|
||||
Example: ` Store the scaffold in a different directory:
|
||||
vela addon init mongodb -p path/to/addon
|
||||
|
||||
If you want to store the scaffold in a different directory, you can use the -p/--path flag:
|
||||
vela addon init mongodb -p ./some/repo --helm-repo-url=https://marketplace.azurecr.io/helm/v1/repo --chart=mongodb --version=12.1.16
|
||||
Add a Helm component:
|
||||
vela addon init mongodb --helm-repo https://marketplace.azurecr.io/helm/v1/repo --chart mongodb --chart-version 12.1.16
|
||||
|
||||
If you don't want the Helm component, just omit the three Chart-related parameters. We will create an empty scaffold for you.
|
||||
vela addon init mongodb`,
|
||||
Add resources from URL using ref-objects component
|
||||
vela addon init my-addon --url https://domain.com/resource.yaml
|
||||
|
||||
Use --no-samples options to skip creating sample files
|
||||
vela addon init my-addon --no-sample
|
||||
|
||||
You can combine all the options together.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("an addon name is required")
|
||||
@@ -421,19 +423,29 @@ If you don't want the Helm component, just omit the three Chart-related paramete
|
||||
return fmt.Errorf("addon name or path should not be empty")
|
||||
}
|
||||
|
||||
// If the user specified all Chart-related info, use the addon template with a Chart in it.
|
||||
if helmRepoURL != "" && chartName != "" && chartVersion != "" {
|
||||
return pkgaddon.CreateAddonFromHelmChart(args[0], addonPath, helmRepoURL, chartName, chartVersion)
|
||||
initCmd := pkgaddon.InitCmd{
|
||||
AddonName: addonName,
|
||||
HelmChartName: chartName,
|
||||
HelmChartVersion: chartVersion,
|
||||
HelmRepoURL: helmRepoURL,
|
||||
Path: addonPath,
|
||||
RefObjURLs: urls,
|
||||
NoSamples: noSample,
|
||||
Overwrite: overwrite,
|
||||
}
|
||||
|
||||
return pkgaddon.CreateAddonSample(addonName, addonPath)
|
||||
return initCmd.CreateScaffold()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&helmRepoURL, "helm-repo-url", "", "URL that points to a Helm repo")
|
||||
cmd.Flags().StringVar(&chartName, "chart", "", "Helm Chart name")
|
||||
cmd.Flags().StringVar(&chartVersion, "version", "", "version of the Chart")
|
||||
cmd.Flags().StringVarP(&path, "path", "p", "", "path to the addon directory (default is ./<addon-name>)")
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&helmRepoURL, "helm-repo", "", "URL that points to a Helm repo")
|
||||
f.StringVar(&chartName, "chart", "", "Helm Chart name")
|
||||
f.StringVar(&chartVersion, "chart-version", "", "version of the Chart")
|
||||
f.StringVarP(&path, "path", "p", "", "path to the addon directory (default is ./<addon-name>)")
|
||||
f.StringArrayVarP(&urls, "url", "u", []string{}, "add URL resources using ref-object component")
|
||||
f.BoolVarP(&noSample, "no-samples", "", false, "do not generate sample files")
|
||||
f.BoolVarP(&overwrite, "force", "f", false, "overwrite existing addon files")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -421,16 +421,16 @@ func TestGenerateParameterString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewAddonCreateCommand(t *testing.T) {
|
||||
cmd := NewAddonCreateCommand()
|
||||
cmd := NewAddonInitCommand()
|
||||
cmd.SetArgs([]string{})
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, "required")
|
||||
|
||||
cmd.SetArgs([]string{"--chart", "a", "--helm-repo-url", "https://some.com", "--version", "c"})
|
||||
cmd.SetArgs([]string{"--chart", "a", "--helm-repo", "https://some.com", "--chart-version", "c"})
|
||||
err = cmd.Execute()
|
||||
assert.ErrorContains(t, err, "required")
|
||||
|
||||
cmd.SetArgs([]string{"test-addon", "--chart", "a", "--helm-repo-url", "https://some.com", "--version", "c"})
|
||||
cmd.SetArgs([]string{"test-addon", "--chart", "a", "--helm-repo", "https://some.com", "--chart-version", "c"})
|
||||
err = cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
_ = os.RemoveAll("test-addon")
|
||||
|
||||
Reference in New Issue
Block a user