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 }