github.com/buildtool/build-tools@v0.2.29-0.20240322150259-6a1d0a553c23/pkg/deploy/deploy.go (about)

     1  // MIT License
     2  //
     3  // Copyright (c) 2018 buildtool
     4  //
     5  // Permission is hereby granted, free of charge, to any person obtaining a copy
     6  // of this software and associated documentation files (the "Software"), to deal
     7  // in the Software without restriction, including without limitation the rights
     8  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9  // copies of the Software, and to permit persons to whom the Software is
    10  // furnished to do so, subject to the following conditions:
    11  //
    12  // The above copyright notice and this permission notice shall be included in all
    13  // copies or substantial portions of the Software.
    14  //
    15  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    21  // SOFTWARE.
    22  
    23  package deploy
    24  
    25  import (
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/apex/log"
    35  
    36  	"github.com/buildtool/build-tools/pkg/file"
    37  
    38  	"github.com/buildtool/build-tools/pkg/args"
    39  	"github.com/buildtool/build-tools/pkg/ci"
    40  	"github.com/buildtool/build-tools/pkg/cli"
    41  	"github.com/buildtool/build-tools/pkg/config"
    42  	"github.com/buildtool/build-tools/pkg/kubectl"
    43  	"github.com/buildtool/build-tools/pkg/version"
    44  )
    45  
    46  type Args struct {
    47  	args.Globals
    48  	Target    string `arg:"" name:"target" help:"the target in the .buildtools.yaml"`
    49  	Context   string `name:"context" short:"c" help:"override the context for default deployment target" default:""`
    50  	Namespace string `name:"namespace" short:"n" help:"override the namespace for default deployment target" default:""`
    51  	Tag       string `name:"tag" help:"override the tag to deploy, not using the CI or VCS evaluated value" default:""`
    52  	Timeout   string `name:"timeout" short:"t" help:"override the default deployment timeout (2 minutes). 0 means forever, all other values should contain a corresponding time unit (e.g. 1s, 2m, 3h)" default:"2m"`
    53  	NoWait    bool   `name:"no-wait" help:"don't wait for deployment to become ready"`
    54  }
    55  
    56  func DoDeploy(dir string, info version.Info, osArgs ...string) int {
    57  	var deployArgs Args
    58  	err := args.ParseArgs(dir, osArgs, info, &deployArgs)
    59  	if err != nil {
    60  		if err != args.Done {
    61  			return -1
    62  		} else {
    63  			return 0
    64  		}
    65  	}
    66  
    67  	if cfg, err := config.Load(dir); err != nil {
    68  		log.Error(err.Error())
    69  		return -1
    70  	} else {
    71  		var env *config.Target
    72  		if env, err = cfg.CurrentTarget(deployArgs.Target); err != nil {
    73  			log.Warnf("%v\n", err)
    74  			env = &config.Target{}
    75  		}
    76  		if deployArgs.Context != "" {
    77  			env.Context = deployArgs.Context
    78  		}
    79  		if env.Context == "" {
    80  			log.Errorf("context is mandatory, not found in configuration for %s and not passed as parameter\n", deployArgs.Target)
    81  			return -5
    82  		}
    83  		if env.Context == "in-cluster" {
    84  			log.Info("Using empty context for in-cluster deploy\n")
    85  			env.Context = ""
    86  		}
    87  		if deployArgs.Namespace != "" {
    88  			env.Namespace = deployArgs.Namespace
    89  		}
    90  		currentCI := cfg.CurrentCI()
    91  		if deployArgs.Tag == "" {
    92  			if !ci.IsValid(currentCI) {
    93  				log.Errorf("Commit and/or branch information is <red>missing</red>. Perhaps your not in a Git repository or forgot to set environment variables?")
    94  				return -3
    95  			}
    96  			deployArgs.Tag = currentCI.Commit()
    97  		} else {
    98  			log.Infof("Using passed tag <green>%s</green> to deploy", deployArgs.Tag)
    99  		}
   100  
   101  		tstamp := time.Now().Format(time.RFC3339)
   102  		client := kubectl.New(env)
   103  		defer client.Cleanup()
   104  		if err := Deploy(dir, cfg.CurrentRegistry().RegistryUrl(), currentCI.BuildName(), tstamp, client, deployArgs); err != nil {
   105  			log.Error(err.Error())
   106  			return -4
   107  
   108  		}
   109  	}
   110  	return 0
   111  }
   112  
   113  func Deploy(dir, registryUrl, buildName, timestamp string, client kubectl.Kubectl, deployArgs Args) error {
   114  	imageName := fmt.Sprintf("%s/%s:%s", registryUrl, buildName, deployArgs.Tag)
   115  
   116  	deploymentFiles := filepath.Join(dir, "k8s")
   117  	if err := processDir(deploymentFiles, deployArgs.Tag, timestamp, deployArgs.Target, imageName, client); err != nil {
   118  		return err
   119  	}
   120  
   121  	if deployArgs.NoWait {
   122  		log.Info("Not waiting for deployment to succeed\n")
   123  		return nil
   124  	}
   125  
   126  	if client.DeploymentExists(buildName) {
   127  		if !client.RolloutStatus(buildName, deployArgs.Timeout) {
   128  			log.Error("Rollout failed. Fetching events.\n")
   129  			log.Error(client.DeploymentEvents(buildName))
   130  			log.Error(client.PodEvents(buildName))
   131  			return fmt.Errorf("failed to rollout")
   132  		}
   133  	}
   134  	return nil
   135  }
   136  
   137  func processDir(dir, commit, timestamp, target, imageName string, client kubectl.Kubectl) error {
   138  	files, err := file.FindFilesForTarget(dir, target)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	scripts, err := file.FindScriptsForTarget(dir, target)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	for _, info := range files {
   147  		if f, err := os.Open(filepath.Join(dir, info.Name())); err != nil {
   148  			return err
   149  		} else {
   150  			if err := processFile(f, commit, timestamp, imageName, client); err != nil {
   151  				return err
   152  			}
   153  		}
   154  	}
   155  	for _, info := range scripts {
   156  		if err := execFile(filepath.Join(dir, info.Name())); err != nil {
   157  			return err
   158  		}
   159  	}
   160  	return nil
   161  }
   162  
   163  func execFile(file string) error {
   164  	cmd := exec.Command(file)
   165  	cmd.Stdout = cli.NewWriter(log.Log)
   166  	cmd.Stderr = cli.NewWriter(log.Log)
   167  	return cmd.Run()
   168  }
   169  
   170  func processFile(file *os.File, commit, timestamp, image string, client kubectl.Kubectl) error {
   171  	if bytes, err := io.ReadAll(file); err != nil {
   172  		return err
   173  	} else {
   174  		content := string(bytes)
   175  		if len(strings.TrimSpace(content)) == 0 {
   176  			log.Debugf("ignoring empty file '<yellow>%s</yellow>'\n", filepath.Base(file.Name()))
   177  			return nil
   178  		}
   179  		r := strings.NewReplacer("${COMMIT}", commit, "${TIMESTAMP}", timestamp, "${IMAGE}", image)
   180  		kubeContent := r.Replace(content)
   181  		log.Debugf("trying to apply: \n---\n%s\n---\n", kubeContent)
   182  		if err := client.Apply(kubeContent); err != nil {
   183  			return err
   184  		}
   185  		return nil
   186  	}
   187  }