diff --git a/build/kubefile/command/command.go b/build/kubefile/command/command.go index beff5281846..13f5ec1ebfb 100644 --- a/build/kubefile/command/command.go +++ b/build/kubefile/command/command.go @@ -14,19 +14,28 @@ package command +import ( + "fmt" +) + const ( // the following commands are kubefile-spec commands // we recommender users to use these commands to pack their // demands for applications. - App = "app" - Launch = "launch" - Cmds = "cmds" - Cmd = "cmd" // Deprecated + App = "app" + Launch = "launch" + Cmds = "cmds" + CNI = "cni" + CSI = "csi" + KUBEVERSION = "kubeversion" Label = "label" Maintainer = "maintainer" + // Deprecated + Cmd = "cmd" + // the following commands are the intenal implementations for kube commands Add = "add" Arg = "arg" @@ -35,17 +44,31 @@ const ( Run = "run" ) +const ( + LabelSupportedKubeVersionAlpha = "app.alpha.sealer.io/supported-kube-version" + LabelSupportedKubeCNIAlpha = "cluster.alpha.sealer.io/kube-cni" + LabelSupportedKubeCSIAlpha = "cluster.alpha.sealer.io/kube-csi" +) + +var ( + LabelKubeCNIPrefix = fmt.Sprintf("%s-", LabelSupportedKubeCNIAlpha) + LabelKubeCSIPrefix = fmt.Sprintf("%s-", LabelSupportedKubeCSIAlpha) +) + // SupportedCommands is list of all Kubefile commands var SupportedCommands = map[string]struct{}{ - Add: {}, - Arg: {}, - Copy: {}, - From: {}, - Label: {}, - Maintainer: {}, - Run: {}, - App: {}, - Launch: {}, - Cmds: {}, - Cmd: {}, + Add: {}, + Arg: {}, + Copy: {}, + From: {}, + Label: {}, + Maintainer: {}, + Run: {}, + App: {}, + Launch: {}, + Cmds: {}, + Cmd: {}, + CNI: {}, + CSI: {}, + KUBEVERSION: {}, } diff --git a/build/kubefile/parser/app_handler.go b/build/kubefile/parser/app_handler.go index f6dc1f56612..c0736de3b62 100644 --- a/build/kubefile/parser/app_handler.go +++ b/build/kubefile/parser/app_handler.go @@ -24,9 +24,10 @@ import ( "github.com/sealerio/sealer/build/kubefile/command" v1 "github.com/sealerio/sealer/pkg/define/application/v1" + "github.com/sealerio/sealer/pkg/define/application/version" ) -func (kp *KubefileParser) processApp(node *Node, result *KubefileResult) error { +func (kp *KubefileParser) processApp(node *Node, result *KubefileResult) (version.VersionedApplication, error) { var ( appName = "" localFiles = []string{} @@ -50,12 +51,12 @@ func (kp *KubefileParser) processApp(node *Node, result *KubefileResult) error { case isRemote(val): remoteFiles = append(remoteFiles, val) default: - return errors.New("source schema should be specified with https:// http:// local:// in APP") + return nil, errors.New("source schema should be specified with https:// http:// local:// in APP") } } if appName == "" { - return errors.New("app name should be specified in the app cmd") + return nil, errors.New("app name should be specified in the app cmd") } // TODO clean the app directory first before putting files into it. @@ -71,12 +72,12 @@ func (kp *KubefileParser) processApp(node *Node, result *KubefileResult) error { if len(remoteFiles) > 0 { remoteCxtAbs, err := os.MkdirTemp(kp.buildContext, "sealer-remote-files") if err != nil { - return errors.Errorf("failed to create remote context: %s", err) + return nil, errors.Errorf("failed to create remote context: %s", err) } files, err := downloadRemoteFiles(remoteCxtAbs, remoteFiles) if err != nil { - return err + return nil, err } remoteCxtBase := filepath.Base(remoteCxtAbs) @@ -97,7 +98,7 @@ func (kp *KubefileParser) processApp(node *Node, result *KubefileResult) error { result.legacyContext.apps2Files[appName] = append([]string{}, filesToCopy...) appType, launchFiles, err := getApplicationType(filesToCopy) if err != nil { - return fmt.Errorf("error in judging the application type: %v", err) + return nil, fmt.Errorf("error in judging the application type: %v", err) } v1App := v1.NewV1Application( @@ -106,7 +107,7 @@ func (kp *KubefileParser) processApp(node *Node, result *KubefileResult) error { launchFiles, ).(*v1.Application) result.Applications[v1App.Name()] = v1App - return nil + return v1App, nil } func downloadRemoteFiles(shadowDir string, files []string) ([]string, error) { diff --git a/build/kubefile/parser/kubefile.go b/build/kubefile/parser/kubefile.go index 514629b3e13..1e47a2f4c3a 100644 --- a/build/kubefile/parser/kubefile.go +++ b/build/kubefile/parser/kubefile.go @@ -19,6 +19,7 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "github.com/sirupsen/logrus" @@ -161,7 +162,14 @@ func (kp *KubefileParser) processOnCmd(result *KubefileResult, node *Node) error result.Dockerfile = mergeLines(result.Dockerfile, node.Original) return nil case command.App: - return kp.processApp(node, result) + _, err := kp.processApp(node, result) + return err + case command.CNI: + return kp.processCNI(node, result) + case command.CSI: + return kp.processCSI(node, result) + case command.KUBEVERSION: + return kp.processKubeVersion(node, result) case command.Launch: return kp.processLaunch(node, result) case command.Cmds: @@ -173,6 +181,33 @@ func (kp *KubefileParser) processOnCmd(result *KubefileResult, node *Node) error } } +func (kp *KubefileParser) processCNI(node *Node, result *KubefileResult) error { + app, err := kp.processApp(node, result) + if err != nil { + return err + } + dockerFileInstruction := fmt.Sprintf(`LABEL %s%s="true"`, command.LabelKubeCNIPrefix, app.Name()) + result.Dockerfile = mergeLines(result.Dockerfile, dockerFileInstruction) + return nil +} + +func (kp *KubefileParser) processCSI(node *Node, result *KubefileResult) error { + app, err := kp.processApp(node, result) + if err != nil { + return err + } + dockerFileInstruction := fmt.Sprintf(`LABEL %s%s="true"`, command.LabelKubeCSIPrefix, app.Name()) + result.Dockerfile = mergeLines(result.Dockerfile, dockerFileInstruction) + return nil +} + +func (kp *KubefileParser) processKubeVersion(node *Node, result *KubefileResult) error { + kubeVersionValue := node.Next.Value + dockerFileInstruction := fmt.Sprintf(`LABEL %s=%s`, command.LabelSupportedKubeVersionAlpha, strconv.Quote(kubeVersionValue)) + result.Dockerfile = mergeLines(result.Dockerfile, dockerFileInstruction) + return nil +} + func (kp *KubefileParser) processCmd(node *Node, result *KubefileResult) error { original := node.Original cmd := strings.Split(original, "CMD ") diff --git a/build/kubefile/parser/parse.go b/build/kubefile/parser/parse.go index 26bf1c9a1a3..7306f43bd40 100644 --- a/build/kubefile/parser/parse.go +++ b/build/kubefile/parser/parse.go @@ -205,17 +205,20 @@ func init() { // functions. Errors are propagated up by Parse() and the resulting AST can // be incorporated directly into the existing AST as a next. dispatch = map[string]func(string, *Directive) (*Node, map[string]bool, error){ - command.Add: parseMaybeJSONToList, - command.Arg: parseNameOrNameVal, - command.Copy: parseMaybeJSONToList, - command.From: parseStringsWhitespaceDelimited, - command.Label: parseLabel, - command.Maintainer: parseString, - command.Run: parseMaybeJSON, - command.App: parseMaybeJSONToList, - command.Launch: parseMaybeJSONToList, - command.Cmds: parseMaybeJSONToList, - command.Cmd: parseMaybeJSONToList, + command.Add: parseMaybeJSONToList, + command.Arg: parseNameOrNameVal, + command.Copy: parseMaybeJSONToList, + command.From: parseStringsWhitespaceDelimited, + command.Label: parseLabel, + command.Maintainer: parseString, + command.Run: parseMaybeJSON, + command.App: parseMaybeJSONToList, + command.KUBEVERSION: parseString, + command.CNI: parseMaybeJSONToList, + command.CSI: parseMaybeJSONToList, + command.Launch: parseMaybeJSONToList, + command.Cmds: parseMaybeJSONToList, + command.Cmd: parseMaybeJSONToList, } } diff --git a/pkg/define/image/v1/sealer_image_extension.go b/pkg/define/image/v1/sealer_image_extension.go index 0837104d1e4..b06027cdde8 100644 --- a/pkg/define/image/v1/sealer_image_extension.go +++ b/pkg/define/image/v1/sealer_image_extension.go @@ -84,6 +84,9 @@ type ImageExtension struct { // applications in the sealer image Applications []version.VersionedApplication `json:"applications,omitempty"` + // Labels are metadata to the sealer image + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + // launch spec will declare Launch Launch `json:"launch,omitempty"` } @@ -108,6 +111,8 @@ type v1ImageExtension struct { SchemaVersion string `json:"schemaVersion"` // sealer image type, like AppImage Type string `json:"type,omitempty"` + // Labels are metadata to the sealer image + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` // applications in the sealer image Applications []application_v1.Application `json:"applications,omitempty"` // launch spec will declare @@ -123,6 +128,7 @@ func (ie *ImageExtension) UnmarshalJSON(data []byte) error { (*ie).BuildClient = v1Ex.BuildClient (*ie).SchemaVersion = v1Ex.SchemaVersion + (*ie).Labels = v1Ex.Labels (*ie).Type = v1Ex.Type (*ie).Applications = make([]version.VersionedApplication, len(v1Ex.Applications)) for i, app := range v1Ex.Applications { diff --git a/pkg/imageengine/buildah/inspect.go b/pkg/imageengine/buildah/inspect.go index 8adf3540746..6e2e67040e9 100644 --- a/pkg/imageengine/buildah/inspect.go +++ b/pkg/imageengine/buildah/inspect.go @@ -19,8 +19,10 @@ import ( "fmt" "os" "regexp" + "strings" "text/template" + "github.com/sealerio/sealer/build/kubefile/command" image_v1 "github.com/sealerio/sealer/pkg/define/image/v1" "github.com/sealerio/sealer/pkg/define/options" @@ -69,6 +71,10 @@ func (engine *Engine) Inspect(opts *options.InspectOptions) error { if err != nil { return errors.Wrapf(err, "failed to get %s in image %s", image_v1.SealerImageExtension, opts.ImageNameOrID) } + imageExtension.Labels = handleImageLabelOutput(builderInfo.OCIv1.Config.Labels) + // NOTE: avoid duplicate content output + builderInfo.OCIv1.Config.Labels = nil + containerImageList, err := GetContainerImagesFromAnnotations(builderInfo.ImageAnnotations) if err != nil { return errors.Wrapf(err, "failed to get %s in image %s", image_v1.SealerImageContainerImageList, opts.ImageNameOrID) @@ -110,3 +116,35 @@ func (engine *Engine) Inspect(opts *options.InspectOptions) error { } return enc.Encode(result) } + +func handleImageLabelOutput(labels map[string]string) map[string]string { + if len(labels) == 0 { + return labels + } + + var result = make(map[string]string) + var supportedCNI []string + var supportedCSI []string + for k, v := range labels { + if strings.HasPrefix(k, command.LabelKubeCNIPrefix) { + supportedCNI = append(supportedCNI, strings.TrimPrefix(k, command.LabelKubeCNIPrefix)) + continue + } + if strings.HasPrefix(k, command.LabelKubeCSIPrefix) { + supportedCSI = append(supportedCSI, strings.TrimPrefix(k, command.LabelKubeCSIPrefix)) + continue + } + result[k] = v + } + + if len(supportedCNI) != 0 { + supportedCNIJSON, _ := json.Marshal(supportedCNI) + result[command.LabelSupportedKubeCNIAlpha] = string(supportedCNIJSON) + } + if len(supportedCSI) != 0 { + supportedCSIJSON, _ := json.Marshal(supportedCSI) + result[command.LabelSupportedKubeCSIAlpha] = string(supportedCSIJSON) + } + + return result +} diff --git a/pkg/imageengine/buildah/inspect_test.go b/pkg/imageengine/buildah/inspect_test.go new file mode 100644 index 00000000000..9020ce5d213 --- /dev/null +++ b/pkg/imageengine/buildah/inspect_test.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Alibaba Group Holding Ltd. +// +// 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 buildah + +import ( + "fmt" + "reflect" + "testing" + + "github.com/sealerio/sealer/build/kubefile/command" +) + +func Test_handleLabelOutput(t *testing.T) { + type args struct { + labels map[string]string + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "", + args: args{ + labels: map[string]string{ + fmt.Sprintf("%s%s", command.LabelKubeCNIPrefix, "calico"): "true", + fmt.Sprintf("%s%s", command.LabelKubeCNIPrefix, "flannel"): "true", + fmt.Sprintf("%s%s", command.LabelKubeCSIPrefix, "alibaba-csi"): "true", + "key1": "value1", + }, + }, + want: map[string]string{ + command.LabelSupportedKubeCNIAlpha: `["calico","flannel"]`, + command.LabelSupportedKubeCSIAlpha: `["alibaba-csi"]`, + "key1": "value1", + }, + }, + { + name: "", + args: args{ + labels: map[string]string{}, + }, + want: map[string]string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := handleImageLabelOutput(tt.args.labels); !reflect.DeepEqual(got, tt.want) { + t.Errorf("handleLabelOutput() = %v, want %v", got, tt.want) + } + }) + } +}