github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/build/kubefile/parser/kubefile.go (about)

     1  // Copyright © 2022 Alibaba Group Holding Ltd.
     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 parser
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"strconv"
    24  	"strings"
    25  
    26  	parse2 "github.com/containers/buildah/pkg/parse"
    27  	"github.com/moby/buildkit/frontend/dockerfile/shell"
    28  	"github.com/pkg/errors"
    29  	"github.com/sealerio/sealer/build/kubefile/command"
    30  	"github.com/sealerio/sealer/pkg/define/application/version"
    31  	"github.com/sealerio/sealer/pkg/define/options"
    32  	"github.com/sealerio/sealer/pkg/imageengine"
    33  	"github.com/sirupsen/logrus"
    34  )
    35  
    36  // LegacyContext stores legacy information during the process of parsing.
    37  // After the parsing ends, return the context to caller, and let the caller
    38  // decide to clean.
    39  type LegacyContext struct {
    40  	files       []string
    41  	directories []string
    42  	// this is a map for appname to related-files
    43  	// only used in test.
    44  	apps2Files map[string][]string
    45  }
    46  
    47  type KubefileResult struct {
    48  	// convert Kubefile to Dockerfile content line by line.
    49  	Dockerfile string
    50  
    51  	// RawCmds to launch sealer image
    52  	// CMDS ["kubectl apply -f recommended.yaml"]
    53  	RawCmds []string
    54  
    55  	// LaunchedAppNames APP name list
    56  	// LAUNCH ["myapp1","myapp2"]
    57  	LaunchedAppNames []string
    58  
    59  	// GlobalEnv is a set of key value pair.
    60  	// set to sealer image some default parameters which is in global level.
    61  	// user could overwrite it through v2.ClusterSpec at run stage.
    62  	GlobalEnv map[string]string
    63  
    64  	// AppEnv is a set of key value pair.
    65  	// it is app level, only this app will be aware of its existence,
    66  	// it is used to render app files, or as an environment variable for app startup and deletion commands
    67  	// it takes precedence over GlobalEnv.
    68  	AppEnvMap map[string]map[string]string
    69  
    70  	// Applications structured APP instruction and register it to this map
    71  	// APP myapp local://app.yaml
    72  	Applications map[string]version.VersionedApplication
    73  
    74  	// AppCmdsMap structured APPCMDS instruction and register it to this map
    75  	// APPCMDS myapp ["kubectl apply -f app.yaml"]
    76  	AppCmdsMap map[string][]string
    77  
    78  	legacyContext LegacyContext
    79  }
    80  
    81  type KubefileParser struct {
    82  	appRootPathFunc func(name string) string
    83  	// path to build context
    84  	buildContext string
    85  	platform     string
    86  	pullPolicy   string
    87  	imageEngine  imageengine.Interface
    88  }
    89  
    90  func (kp *KubefileParser) ParseKubefile(rwc io.Reader) (*KubefileResult, error) {
    91  	result, err := parse(rwc)
    92  	if err != nil {
    93  		return nil, fmt.Errorf("failed to parse dockerfile: %v", err)
    94  	}
    95  
    96  	mainNode := result.AST
    97  	return kp.generateResult(mainNode)
    98  }
    99  
   100  func (kp *KubefileParser) generateResult(mainNode *Node) (*KubefileResult, error) {
   101  	var (
   102  		result = &KubefileResult{
   103  			Applications: map[string]version.VersionedApplication{},
   104  			AppCmdsMap:   map[string][]string{},
   105  			GlobalEnv:    map[string]string{},
   106  			AppEnvMap:    map[string]map[string]string{},
   107  			legacyContext: LegacyContext{
   108  				files:       []string{},
   109  				directories: []string{},
   110  				apps2Files:  map[string][]string{},
   111  			},
   112  			RawCmds:          []string{},
   113  			LaunchedAppNames: []string{},
   114  		}
   115  
   116  		err error
   117  
   118  		launchCnt = 0
   119  		cmdsCnt   = 0
   120  		cmdCnt    = 0
   121  	)
   122  
   123  	defer func() {
   124  		if err != nil {
   125  			if err2 := result.CleanLegacyContext(); err2 != nil {
   126  				logrus.Warn(err2)
   127  			}
   128  		}
   129  	}()
   130  
   131  	// pre-action for commands
   132  	// for FROM, it will try to pull the image, and get apps from "FROM" image
   133  	// for LAUNCH, it will check if it's the last line
   134  	for i, node := range mainNode.Children {
   135  		_command := node.Value
   136  		if _, ok := command.SupportedCommands[_command]; !ok {
   137  			return nil, errors.Errorf("command %s is not supported", _command)
   138  		}
   139  
   140  		switch _command {
   141  		case command.From:
   142  			// process FROM aims to pull the image, and merge the applications from
   143  			// the FROM image.
   144  			if err = kp.processFrom(node, result); err != nil {
   145  				return nil, fmt.Errorf("failed to process from: %v", err)
   146  			}
   147  		case command.Launch:
   148  			launchCnt++
   149  			if launchCnt > 1 {
   150  				return nil, errors.New("only one launch could be specified")
   151  			}
   152  			if i != len(mainNode.Children)-1 {
   153  				return nil, errors.New("launch should be the last instruction")
   154  			}
   155  		case command.Cmds:
   156  			cmdsCnt++
   157  			if cmdsCnt > 1 {
   158  				return nil, errors.New("only one cmds could be specified")
   159  			}
   160  			if i != len(mainNode.Children)-1 {
   161  				return nil, errors.New("cmds should be the last instruction")
   162  			}
   163  
   164  		case command.Cmd:
   165  			cmdCnt++
   166  			if cmdCnt > 1 {
   167  				break
   168  			}
   169  			logrus.Warn("CMD is about to be deprecated.")
   170  		}
   171  
   172  		if cmdCnt >= 1 && launchCnt == 1 {
   173  			return nil, errors.New("cmd and launch are mutually exclusive")
   174  		}
   175  
   176  		if err = kp.processOnCmd(result, node); err != nil {
   177  			return nil, err
   178  		}
   179  	}
   180  
   181  	// check result validation
   182  	// if no app type detected and no AppCmds exist for this app, will return error.
   183  	for name, registered := range result.Applications {
   184  		if registered.Type() != "" {
   185  			continue
   186  		}
   187  
   188  		if _, ok := result.AppCmdsMap[name]; !ok {
   189  			return nil, fmt.Errorf("app %s need to specify APPCMDS if no app type detected", name)
   190  		}
   191  	}
   192  
   193  	// register app with app env list.
   194  	for appName, appEnv := range result.AppEnvMap {
   195  		app := result.Applications[appName]
   196  		app.SetEnv(appEnv)
   197  		result.Applications[appName] = app
   198  	}
   199  
   200  	// register app with app cmds.
   201  	for appName, appCmds := range result.AppCmdsMap {
   202  		app := result.Applications[appName]
   203  		app.SetCmds(appCmds)
   204  		result.Applications[appName] = app
   205  	}
   206  
   207  	return result, nil
   208  }
   209  
   210  func (kp *KubefileParser) processOnCmd(result *KubefileResult, node *Node) error {
   211  	cmd := node.Value
   212  	switch cmd {
   213  	case command.Label, command.Maintainer, command.Add, command.Arg, command.From, command.Run:
   214  		result.Dockerfile = mergeLines(result.Dockerfile, node.Original)
   215  		return nil
   216  	case command.Env:
   217  		// update global env to dockerfile at the same,	for using it at build stage.
   218  		result.Dockerfile = mergeLines(result.Dockerfile, node.Original)
   219  		return kp.processGlobalEnv(node, result)
   220  	case command.AppEnv:
   221  		return kp.processAppEnv(node, result)
   222  	case command.App:
   223  		_, err := kp.processApp(node, result)
   224  		return err
   225  	case command.AppCmds:
   226  		return kp.processAppCmds(node, result)
   227  	case command.CNI:
   228  		return kp.processCNI(node, result)
   229  	case command.CSI:
   230  		return kp.processCSI(node, result)
   231  	case command.KUBEVERSION:
   232  		return kp.processKubeVersion(node, result)
   233  	case command.Launch:
   234  		return kp.processLaunch(node, result)
   235  	case command.Cmds:
   236  		return kp.processCmds(node, result)
   237  	case command.Copy:
   238  		return kp.processCopy(node, result)
   239  	case command.Cmd:
   240  		return kp.processCmd(node, result)
   241  	default:
   242  		return fmt.Errorf("failed to recognize cmd: %s", cmd)
   243  	}
   244  }
   245  
   246  func (kp *KubefileParser) processCNI(node *Node, result *KubefileResult) error {
   247  	app, err := kp.processApp(node, result)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	dockerFileInstruction := fmt.Sprintf(`LABEL %s%s="true"`, command.LabelKubeCNIPrefix, app.Name())
   252  	result.Dockerfile = mergeLines(result.Dockerfile, dockerFileInstruction)
   253  	return nil
   254  }
   255  
   256  func (kp *KubefileParser) processCSI(node *Node, result *KubefileResult) error {
   257  	app, err := kp.processApp(node, result)
   258  	if err != nil {
   259  		return err
   260  	}
   261  	dockerFileInstruction := fmt.Sprintf(`LABEL %s%s="true"`, command.LabelKubeCSIPrefix, app.Name())
   262  	result.Dockerfile = mergeLines(result.Dockerfile, dockerFileInstruction)
   263  	return nil
   264  }
   265  
   266  func (kp *KubefileParser) processKubeVersion(node *Node, result *KubefileResult) error {
   267  	kubeVersionValue := node.Next.Value
   268  	dockerFileInstruction := fmt.Sprintf(`LABEL %s=%s`, command.LabelSupportedKubeVersionAlpha, strconv.Quote(kubeVersionValue))
   269  	result.Dockerfile = mergeLines(result.Dockerfile, dockerFileInstruction)
   270  	return nil
   271  }
   272  
   273  func (kp *KubefileParser) processCopy(node *Node, result *KubefileResult) error {
   274  	if node.Next == nil || node.Next.Next == nil {
   275  		return fmt.Errorf("line %d: invalid copy instruction: %s", node.StartLine, node.Original)
   276  	}
   277  
   278  	copySrc := node.Next.Value
   279  	copyDest := node.Next.Next.Value
   280  	// support ${arch} on Kubefile COPY instruction
   281  	// For example:
   282  	// if arch is amd64
   283  	// `COPY ${ARCH}/* .` will be mutated to `COPY amd64/* .`
   284  	// `COPY $ARCH/* .` will be mutated to `COPY amd64/* .`
   285  	_, arch, _, err := parse2.Platform(kp.platform)
   286  	if err != nil {
   287  		return fmt.Errorf("failed to parse platform: %v", err)
   288  	}
   289  
   290  	ex := shell.NewLex('\\')
   291  	src, err := ex.ProcessWordWithMap(copySrc, map[string]string{"ARCH": arch})
   292  	if err != nil {
   293  		return fmt.Errorf("failed to render COPY instruction: %v", err)
   294  	}
   295  
   296  	tmpLine := strings.Join(append([]string{command.Copy}, src, copyDest), " ")
   297  	result.Dockerfile = mergeLines(result.Dockerfile, tmpLine)
   298  
   299  	return nil
   300  }
   301  
   302  func (kp *KubefileParser) processAppCmds(node *Node, result *KubefileResult) error {
   303  	appNode := node.Next
   304  	appName := appNode.Value
   305  
   306  	if appName == "" {
   307  		return errors.New("app name should be specified in the APPCMDS instruction")
   308  	}
   309  
   310  	tmpPrefix := fmt.Sprintf("%s %s", strings.TrimSpace(strings.ToUpper(command.AppCmds)), strings.TrimSpace(appName))
   311  	appCmdsStr := strings.TrimSpace(strings.TrimPrefix(node.Original, tmpPrefix))
   312  
   313  	var appCmds []string
   314  	if err := json.Unmarshal([]byte(appCmdsStr), &appCmds); err != nil {
   315  		return errors.Wrapf(err, `the APPCMDS value should be format: APPCMDS appName ["executable","param1","param2","..."]`)
   316  	}
   317  
   318  	// check whether the app name exist
   319  	var appExisted bool
   320  	for existAppName := range result.Applications {
   321  		if existAppName == appName {
   322  			appExisted = true
   323  		}
   324  	}
   325  	if !appExisted {
   326  		return fmt.Errorf("the specified app name(%s) for `APPCMDS` should be exist", appName)
   327  	}
   328  
   329  	result.AppCmdsMap[appName] = appCmds
   330  	return nil
   331  }
   332  
   333  func (kp *KubefileParser) processAppEnv(node *Node, result *KubefileResult) error {
   334  	var (
   335  		appName = ""
   336  		envList []string
   337  	)
   338  
   339  	// first node value is the command
   340  	for ptr := node.Next; ptr != nil; ptr = ptr.Next {
   341  		val := ptr.Value
   342  		// record the first word to be the app name
   343  		if appName == "" {
   344  			appName = val
   345  			continue
   346  		}
   347  		envList = append(envList, val)
   348  	}
   349  
   350  	if appName == "" {
   351  		return errors.New("app name should be specified in the APPENV instruction")
   352  	}
   353  
   354  	if _, ok := result.Applications[appName]; !ok {
   355  		return fmt.Errorf("the specified app name(%s) for `APPENV` should be exist", appName)
   356  	}
   357  
   358  	tmpEnv := make(map[string]string)
   359  	for _, elem := range envList {
   360  		var kv []string
   361  		if kv = strings.SplitN(elem, "=", 2); len(kv) != 2 {
   362  			continue
   363  		}
   364  		tmpEnv[kv[0]] = kv[1]
   365  	}
   366  
   367  	appEnv := result.AppEnvMap[appName]
   368  	if appEnv == nil {
   369  		appEnv = make(map[string]string)
   370  	}
   371  
   372  	for k, v := range tmpEnv {
   373  		appEnv[k] = v
   374  	}
   375  
   376  	result.AppEnvMap[appName] = appEnv
   377  	return nil
   378  }
   379  
   380  func (kp *KubefileParser) processGlobalEnv(node *Node, result *KubefileResult) error {
   381  	valueList := strings.SplitN(node.Original, "ENV ", 2)
   382  	if len(valueList) != 2 {
   383  		return fmt.Errorf("line %d: invalid ENV instruction: %s", node.StartLine, node.Original)
   384  	}
   385  	envs := valueList[1]
   386  
   387  	for _, elem := range strings.Split(envs, " ") {
   388  		if elem == "" {
   389  			continue
   390  		}
   391  
   392  		var kv []string
   393  		if kv = strings.SplitN(elem, "=", 2); len(kv) != 2 {
   394  			continue
   395  		}
   396  		result.GlobalEnv[kv[0]] = kv[1]
   397  	}
   398  
   399  	return nil
   400  }
   401  
   402  func (kp *KubefileParser) processCmd(node *Node, result *KubefileResult) error {
   403  	original := node.Original
   404  	cmd := strings.Split(original, "CMD ")
   405  	node.Next.Value = cmd[1]
   406  	result.RawCmds = append(result.RawCmds, node.Next.Value)
   407  	return nil
   408  }
   409  
   410  func (kp *KubefileParser) processCmds(node *Node, result *KubefileResult) error {
   411  	cmdsNode := node.Next
   412  	for iter := cmdsNode; iter != nil; iter = iter.Next {
   413  		result.RawCmds = append(result.RawCmds, iter.Value)
   414  	}
   415  	return nil
   416  }
   417  
   418  func (kp *KubefileParser) processLaunch(node *Node, result *KubefileResult) error {
   419  	appNode := node.Next
   420  	for iter := appNode; iter != nil; iter = iter.Next {
   421  		appName := iter.Value
   422  		appName = strings.TrimSpace(appName)
   423  		if _, ok := result.Applications[appName]; !ok {
   424  			return errors.Errorf("application %s does not exist in the image", appName)
   425  		}
   426  		result.LaunchedAppNames = append(result.LaunchedAppNames, appName)
   427  	}
   428  
   429  	return nil
   430  }
   431  
   432  func (kp *KubefileParser) processFrom(node *Node, result *KubefileResult) error {
   433  	var (
   434  		platform  = parse2.DefaultPlatform()
   435  		flags     = node.Flags
   436  		imageNode = node.Next
   437  	)
   438  	if len(flags) > 0 {
   439  		f, err := parseListFlag(flags[0])
   440  		if err != nil {
   441  			return err
   442  		}
   443  		if f.flag != "platform" {
   444  			return errors.Errorf("flag %s is not available in FROM", f.flag)
   445  		}
   446  		platform = f.items[0]
   447  	}
   448  
   449  	if imageNode == nil || len(imageNode.Value) == 0 {
   450  		return errors.Errorf("image should be specified in the FROM")
   451  	}
   452  	image := imageNode.Value
   453  	if image == "scratch" {
   454  		return nil
   455  	}
   456  
   457  	id, err := kp.imageEngine.Pull(&options.PullOptions{
   458  		PullPolicy: kp.pullPolicy,
   459  		Image:      image,
   460  		Platform:   platform,
   461  	})
   462  	if err != nil {
   463  		return fmt.Errorf("failed to pull image %s: %v", image, err)
   464  	}
   465  
   466  	imageSpec, err := kp.imageEngine.Inspect(&options.InspectOptions{ImageNameOrID: id})
   467  	if err != nil {
   468  		return fmt.Errorf("failed to get image-extension %s: %s", image, err)
   469  	}
   470  
   471  	for _, app := range imageSpec.ImageExtension.Applications {
   472  		// for range has problem.
   473  		// can't assign address to the target.
   474  		// we should use temp value.
   475  		// https://github.com/golang/gofrontend/blob/e387439bfd24d5e142874b8e68e7039f74c744d7/go/statements.cc#L5501
   476  		theApp := app
   477  		result.Applications[app.Name()] = theApp
   478  	}
   479  
   480  	return nil
   481  }
   482  
   483  func (kr *KubefileResult) CleanLegacyContext() error {
   484  	var (
   485  		lc  = kr.legacyContext
   486  		err error
   487  	)
   488  
   489  	for _, f := range lc.files {
   490  		err = os.Remove(f)
   491  	}
   492  
   493  	for _, dir := range lc.directories {
   494  		err = os.RemoveAll(dir)
   495  	}
   496  
   497  	return errors.Wrap(err, "failed to clean legacy context")
   498  }
   499  
   500  func NewParser(appRootPath string,
   501  	buildOptions options.BuildOptions,
   502  	imageEngine imageengine.Interface,
   503  	platform string) *KubefileParser {
   504  	return &KubefileParser{
   505  		// application will be put under approot/name/
   506  		appRootPathFunc: func(name string) string {
   507  			return makeItDir(filepath.Join(appRootPath, name))
   508  		},
   509  		imageEngine:  imageEngine,
   510  		buildContext: buildOptions.ContextDir,
   511  		pullPolicy:   buildOptions.PullPolicy,
   512  		platform:     platform,
   513  	}
   514  }