github.com/splunk/dan1-qbec@v0.7.3/internal/commands/init.go (about)

     1  /*
     2     Copyright 2019 Splunk Inc.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package commands
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"html/template"
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  
    27  	"github.com/ghodss/yaml"
    28  	"github.com/spf13/cobra"
    29  	"github.com/splunk/qbec/internal/model"
    30  	"github.com/splunk/qbec/internal/remote"
    31  	"github.com/splunk/qbec/internal/sio"
    32  )
    33  
    34  type initCommandConfig struct {
    35  	withExample bool // create a hello world example
    36  }
    37  
    38  var baseParamsTemplate = template.Must(template.New("base").Parse(`
    39  // this file has the baseline default parameters
    40  {
    41    components: { {{- if .AddExample}}
    42      hello: {
    43        indexData: 'hello baseline\n',
    44        replicas: 1,
    45      }, {{- end}}
    46    },
    47  }
    48  `))
    49  
    50  var envParamsTemplate = template.Must(template.New("env").Parse(`
    51  // this file has the param overrides for the default environment
    52  local base = import './base.libsonnet';
    53  
    54  base {
    55    components +: { {{- if .AddExample}}
    56      hello +: {
    57        indexData: 'hello default\n',
    58        replicas: 2,
    59      }, {{- end}}
    60    }
    61  }
    62  `))
    63  
    64  var paramsTemplate = template.Must(template.New("any-env").Parse(`
    65  // this file returns the params for the current qbec environment
    66  // you need to add an entry here every time you add a new environment.
    67  
    68  local env = std.extVar('qbec.io/env');
    69  local paramsMap = {
    70    _: import './environments/base.libsonnet',
    71    default: import './environments/default.libsonnet',
    72  };
    73  
    74  if std.objectHas(paramsMap, env) then paramsMap[env] else error 'environment ' + env + ' not defined in ' + std.thisFile
    75  
    76  `))
    77  
    78  var componentExampleTemplate = template.Must(template.New("comp").Parse(`
    79  local p = import '../params.libsonnet';
    80  local params = p.components.hello;
    81  
    82  [
    83    {
    84      apiVersion: 'v1',
    85      kind: 'ConfigMap',
    86      metadata: {
    87        name: 'demo-config',
    88      },
    89      data: {
    90        'index.html': params.indexData,
    91      },
    92    },
    93    {
    94      apiVersion: 'apps/v1',
    95      kind: 'Deployment',
    96      metadata: {
    97        name: 'demo-deploy',
    98        labels: {
    99          app: 'demo-deploy',
   100        },
   101      },
   102      spec: {
   103        replicas: params.replicas,
   104        selector: {
   105          matchLabels: {
   106            app: 'demo-deploy',
   107          },
   108        },
   109        template: {
   110          metadata: {
   111            labels: {
   112              app: 'demo-deploy',
   113            },
   114          },
   115          spec: {
   116            containers: [
   117              {
   118                name: 'main',
   119                image: 'nginx:stable',
   120                imagePullPolicy: 'Always',
   121                volumeMounts: [
   122                  {
   123                    name: 'web',
   124                    mountPath: '/usr/share/nginx/html',
   125                  },
   126                ],
   127              },
   128            ],
   129            volumes: [
   130              {
   131                name: 'web',
   132                configMap: {
   133                  name: 'demo-config',
   134                },
   135              },
   136            ],
   137          },
   138        },
   139      },
   140    },
   141  ]
   142  `))
   143  
   144  func writeTemplateFile(file string, t *template.Template, data interface{}) error {
   145  	var w bytes.Buffer
   146  	if err := t.Execute(&w, data); err != nil {
   147  		return fmt.Errorf("unable to expand template for file %s, %v", file, err)
   148  	}
   149  	if err := ioutil.WriteFile(file, w.Bytes(), 0644); err != nil {
   150  		return err
   151  	}
   152  	sio.Noticeln("wrote", file)
   153  	return nil
   154  }
   155  
   156  func writeFiles(dir string, app model.QbecApp, config initCommandConfig) error {
   157  	if err := os.Mkdir(app.Metadata.Name, 0755); err != nil {
   158  		return err
   159  	}
   160  
   161  	templateData := struct {
   162  		AddExample bool
   163  	}{config.withExample}
   164  
   165  	type templateFile struct {
   166  		t *template.Template
   167  		f string
   168  	}
   169  
   170  	compsDir, envDir := filepath.Join(dir, "components"), filepath.Join(dir, "environments")
   171  	for _, dir := range []string{compsDir, envDir} {
   172  		if err := os.MkdirAll(dir, 0755); err != nil {
   173  			return err
   174  		}
   175  	}
   176  	files := []templateFile{
   177  		{
   178  			t: paramsTemplate,
   179  			f: filepath.Join(dir, "params.libsonnet"),
   180  		},
   181  		{
   182  			t: baseParamsTemplate,
   183  			f: filepath.Join(envDir, "base.libsonnet"),
   184  		},
   185  		{
   186  			t: envParamsTemplate,
   187  			f: filepath.Join(envDir, "default.libsonnet"),
   188  		},
   189  	}
   190  	if config.withExample {
   191  		files = append(files, templateFile{
   192  			t: componentExampleTemplate,
   193  			f: filepath.Join(compsDir, "hello.jsonnet"),
   194  		})
   195  	}
   196  	for _, tf := range files {
   197  		if err := writeTemplateFile(tf.f, tf.t, templateData); err != nil {
   198  			return err
   199  		}
   200  	}
   201  
   202  	b, err := yaml.Marshal(app)
   203  	if err != nil {
   204  		return fmt.Errorf("yaml marshal: %v", err)
   205  	}
   206  	file := filepath.Join(dir, "qbec.yaml")
   207  	if err := ioutil.WriteFile(file, b, 0644); err != nil {
   208  		return err
   209  	}
   210  	sio.Noticeln("wrote", file)
   211  	return nil
   212  }
   213  
   214  func doInit(args []string, config initCommandConfig) error {
   215  	if len(args) != 1 {
   216  		return fmt.Errorf("a single app name argument must be supplied")
   217  	}
   218  	name := args[0]
   219  	_, err := os.Stat(name)
   220  	if err == nil {
   221  		return fmt.Errorf("directory %s already exists", name)
   222  	}
   223  	if !os.IsNotExist(err) {
   224  		return err
   225  	}
   226  
   227  	ctx, err := remote.CurrentContextInfo()
   228  	if err != nil {
   229  		sio.Warnf("could not get current K8s context info, %v\n", err)
   230  		sio.Warnln("using fake parameters for the default environment")
   231  		ctx = &remote.ContextInfo{
   232  			ServerURL: "https://minikube",
   233  		}
   234  	}
   235  	sio.Noticef("using server URL %q and default namespace %q for the default environment\n", ctx.ServerURL, ctx.Namespace)
   236  	app := model.QbecApp{
   237  		Kind:       "App",
   238  		APIVersion: model.LatestAPIVersion,
   239  		Metadata: model.AppMeta{
   240  			Name: name,
   241  		},
   242  		Spec: model.AppSpec{
   243  			Environments: map[string]model.Environment{
   244  				"default": {
   245  					Server:           ctx.ServerURL,
   246  					DefaultNamespace: ctx.Namespace,
   247  				},
   248  			},
   249  		},
   250  	}
   251  	return writeFiles(name, app, config)
   252  }
   253  
   254  func newInitCommand() *cobra.Command {
   255  	cmd := &cobra.Command{
   256  		Use:   "init <app-name>",
   257  		Short: "initialize a qbec app",
   258  	}
   259  
   260  	config := initCommandConfig{}
   261  	cmd.Flags().BoolVar(&config.withExample, "with-example", false, "create a hello world sample component")
   262  
   263  	cmd.RunE = func(c *cobra.Command, args []string) error {
   264  		return wrapError(doInit(args, config))
   265  	}
   266  	return cmd
   267  }