apply.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. package cmd
  2. import (
  3. "context"
  4. "fmt"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "strconv"
  9. "github.com/cli/cli/git"
  10. "github.com/fatih/color"
  11. "github.com/mitchellh/mapstructure"
  12. api "github.com/porter-dev/porter/api/client"
  13. "github.com/porter-dev/porter/api/types"
  14. "github.com/porter-dev/porter/cli/cmd/deploy"
  15. "github.com/porter-dev/porter/internal/templater/utils"
  16. "github.com/porter-dev/switchboard/pkg/drivers"
  17. "github.com/porter-dev/switchboard/pkg/models"
  18. "github.com/porter-dev/switchboard/pkg/parser"
  19. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  20. "github.com/porter-dev/switchboard/pkg/worker"
  21. "github.com/rs/zerolog"
  22. "github.com/spf13/cobra"
  23. )
  24. // applyCmd represents the "porter apply" base command when called
  25. // with a porter.yaml file as an argument
  26. var applyCmd = &cobra.Command{
  27. Use: "apply",
  28. Short: "Applies a configuration to an application",
  29. Long: fmt.Sprintf(`
  30. %s
  31. Applies a configuration to an application by either creating a new one or updating an existing
  32. one. For example:
  33. %s
  34. This command will apply the configuration contained in porter.yaml to the requested project and
  35. cluster either provided inside the porter.yaml file or through environment variables. Note that
  36. environment variables will always take precendence over values specified in the porter.yaml file.
  37. By default, this command expects to be run from a local git repository.
  38. The following are the environment variables that can be used to set certain values while
  39. applying a configuration:
  40. PORTER_CLUSTER Cluster ID that contains the project
  41. PORTER_PROJECT Project ID that contains the application
  42. PORTER_NAMESPACE The Kubernetes namespace that the application belongs to
  43. PORTER_SOURCE_NAME Name of the source Helm chart
  44. PORTER_SOURCE_REPO The URL of the Helm charts registry
  45. PORTER_SOURCE_VERSION The version of the Helm chart to use
  46. PORTER_TAG The Docker image tag to use (like the git commit hash)
  47. `,
  48. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter apply\":"),
  49. color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
  50. ),
  51. Run: func(cmd *cobra.Command, args []string) {
  52. err := checkLoginAndRun(args, apply)
  53. if err != nil {
  54. os.Exit(1)
  55. }
  56. },
  57. }
  58. var porterYAML string
  59. func init() {
  60. rootCmd.AddCommand(applyCmd)
  61. applyCmd.Flags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
  62. applyCmd.MarkFlagRequired("file")
  63. }
  64. func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  65. fileBytes, err := ioutil.ReadFile(porterYAML)
  66. if err != nil {
  67. return err
  68. }
  69. resGroup, err := parser.ParseRawBytes(fileBytes)
  70. if err != nil {
  71. return err
  72. }
  73. basePath, err := os.Getwd()
  74. if err != nil {
  75. return err
  76. }
  77. worker := worker.NewWorker()
  78. worker.RegisterDriver("porter.deploy", NewPorterDriver)
  79. worker.SetDefaultDriver("porter.deploy")
  80. return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
  81. BasePath: basePath,
  82. })
  83. }
  84. type Source struct {
  85. Name string
  86. Repo string
  87. Version string
  88. }
  89. type Target struct {
  90. Project uint
  91. Cluster uint
  92. Namespace string
  93. }
  94. type Config struct {
  95. Build struct {
  96. Method string
  97. Context string
  98. Dockerfile string
  99. }
  100. Values map[string]interface{}
  101. }
  102. type Driver struct {
  103. source *Source
  104. target *Target
  105. config *Config
  106. output map[string]interface{}
  107. lookupTable *map[string]drivers.Driver
  108. logger *zerolog.Logger
  109. }
  110. func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
  111. driver := &Driver{
  112. lookupTable: opts.DriverLookupTable,
  113. logger: opts.Logger,
  114. output: make(map[string]interface{}),
  115. }
  116. source, err := getSource(resource.Source)
  117. if err != nil {
  118. return nil, err
  119. }
  120. driver.source = source
  121. target, err := getTarget(resource.Target)
  122. if err != nil {
  123. return nil, err
  124. }
  125. driver.target = target
  126. resourceConfig, err := getConfig(resource.Config)
  127. if err != nil {
  128. return nil, err
  129. }
  130. driver.config = resourceConfig
  131. chart, err := GetAPIClient(config).GetTemplate(
  132. context.Background(),
  133. driver.source.Name,
  134. driver.source.Version,
  135. &types.GetTemplateRequest{
  136. TemplateGetBaseRequest: types.TemplateGetBaseRequest{
  137. RepoURL: driver.source.Repo,
  138. },
  139. },
  140. )
  141. if err != nil {
  142. return nil, err
  143. }
  144. driver.output[resource.Name] = utils.CoalesceValues(chart.Values, driver.config.Values)
  145. return driver, nil
  146. }
  147. func (d *Driver) ShouldApply(resource *models.Resource) bool {
  148. return true
  149. }
  150. func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
  151. client := GetAPIClient(config)
  152. if resource.Name == "" {
  153. return nil, fmt.Errorf("empty app name")
  154. }
  155. resource.Name = fmt.Sprintf("preview-%s", resource.Name)
  156. namespace := d.target.Namespace
  157. existingNamespaces, err := client.GetK8sNamespaces(context.Background(), d.target.Project, d.target.Cluster)
  158. if err != nil {
  159. return nil, err
  160. }
  161. namespaceFound := false
  162. for _, ns := range existingNamespaces.Items {
  163. if namespace == ns.Name {
  164. namespaceFound = true
  165. break
  166. }
  167. }
  168. if !namespaceFound {
  169. _, err := client.CreateNewK8sNamespace(
  170. context.Background(), d.target.Project, d.target.Cluster, namespace)
  171. if err != nil {
  172. return nil, err
  173. }
  174. }
  175. method := d.config.Build.Method
  176. if method == "" {
  177. return nil, fmt.Errorf("method should either be \"docker\" or \"pack\"")
  178. }
  179. fullPath, err := filepath.Abs(d.config.Build.Context)
  180. if err != nil {
  181. return nil, err
  182. }
  183. tag := os.Getenv("PORTER_TAG")
  184. if tag == "" {
  185. commit, err := git.LastCommit()
  186. if err != nil {
  187. return nil, err
  188. }
  189. tag = commit.Sha
  190. }
  191. if tag == "" {
  192. return nil, fmt.Errorf("could not find commit SHA to tag the image")
  193. }
  194. _, err = client.GetRelease(context.Background(), d.target.Project,
  195. d.target.Cluster, d.target.Namespace, resource.Name)
  196. if err != nil {
  197. // create new release
  198. color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
  199. regList, err := client.ListRegistries(context.Background(), d.target.Project)
  200. if err != nil {
  201. return nil, err
  202. }
  203. var registryURL string
  204. if len(*regList) == 0 {
  205. return nil, fmt.Errorf("no registry found")
  206. } else {
  207. registryURL = (*regList)[0].URL
  208. }
  209. createAgent := &deploy.CreateAgent{
  210. Client: client,
  211. CreateOpts: &deploy.CreateOpts{
  212. SharedOpts: &deploy.SharedOpts{
  213. ProjectID: d.target.Project,
  214. ClusterID: d.target.Cluster,
  215. Namespace: namespace,
  216. LocalPath: fullPath,
  217. LocalDockerfile: d.config.Build.Dockerfile,
  218. Method: deploy.DeployBuildType(method),
  219. },
  220. Kind: d.source.Name,
  221. ReleaseName: resource.Name,
  222. RegistryURL: registryURL,
  223. },
  224. }
  225. subdomain, err := createAgent.CreateFromDocker(d.config.Values, tag)
  226. return resource, handleSubdomainCreate(subdomain, err)
  227. }
  228. // update an existing release
  229. color.New(color.FgGreen).Println("Deploying app:", resource.Name)
  230. updateAgent, err := deploy.NewDeployAgent(client, resource.Name, &deploy.DeployOpts{
  231. SharedOpts: &deploy.SharedOpts{
  232. ProjectID: d.target.Project,
  233. ClusterID: d.target.Cluster,
  234. Namespace: namespace,
  235. LocalPath: fullPath,
  236. LocalDockerfile: d.config.Build.Dockerfile,
  237. OverrideTag: tag,
  238. Method: deploy.DeployBuildType(method),
  239. },
  240. Local: true,
  241. })
  242. if err != nil {
  243. return nil, err
  244. }
  245. buildEnv, err := updateAgent.GetBuildEnv()
  246. if err != nil {
  247. return nil, err
  248. }
  249. err = updateAgent.SetBuildEnv(buildEnv)
  250. if err != nil {
  251. return nil, err
  252. }
  253. err = updateAgent.Build()
  254. if err != nil {
  255. return nil, err
  256. }
  257. err = updateAgent.Push()
  258. if err != nil {
  259. return nil, err
  260. }
  261. err = updateAgent.UpdateImageAndValues(d.config.Values)
  262. if err != nil {
  263. return nil, err
  264. }
  265. return resource, nil
  266. }
  267. func (d *Driver) Output() (map[string]interface{}, error) {
  268. return d.output, nil
  269. }
  270. func getSource(genericSource map[string]interface{}) (*Source, error) {
  271. source := &Source{}
  272. // first read from env vars
  273. source.Name = os.Getenv("PORTER_SOURCE_NAME")
  274. source.Repo = os.Getenv("PORTER_SOURCE_REPO")
  275. source.Version = os.Getenv("PORTER_SOURCE_VERSION")
  276. // next, check for values in the YAML file
  277. if source.Name == "" {
  278. if name, ok := genericSource["name"]; ok {
  279. nameVal, ok := name.(string)
  280. if !ok {
  281. return nil, fmt.Errorf("invalid name provided")
  282. }
  283. source.Name = nameVal
  284. }
  285. }
  286. if source.Name == "" {
  287. return nil, fmt.Errorf("source name required")
  288. }
  289. if _, ok := supportedKinds[source.Name]; !ok {
  290. return nil, fmt.Errorf("%s is not a supported source name: specify web, job, or worker", source.Name)
  291. }
  292. if source.Repo == "" {
  293. if repo, ok := genericSource["repo"]; ok {
  294. repoVal, ok := repo.(string)
  295. if !ok {
  296. return nil, fmt.Errorf("invalid repo provided")
  297. }
  298. source.Repo = repoVal
  299. }
  300. }
  301. if source.Version == "" {
  302. if version, ok := genericSource["version"]; ok {
  303. versionVal, ok := version.(string)
  304. if !ok {
  305. return nil, fmt.Errorf("invalid version provided")
  306. }
  307. source.Version = versionVal
  308. }
  309. }
  310. // lastly, just put in the defaults
  311. if source.Repo == "" {
  312. source.Repo = "https://charts.getporter.dev"
  313. }
  314. if source.Version == "" {
  315. source.Version = "latest"
  316. }
  317. return source, nil
  318. }
  319. func getTarget(genericTarget map[string]interface{}) (*Target, error) {
  320. target := &Target{}
  321. // first read from env vars
  322. if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
  323. project, err := strconv.Atoi(projectEnv)
  324. if err != nil {
  325. return nil, err
  326. }
  327. target.Project = uint(project)
  328. }
  329. if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
  330. cluster, err := strconv.Atoi(clusterEnv)
  331. if err != nil {
  332. return nil, err
  333. }
  334. target.Cluster = uint(cluster)
  335. }
  336. target.Namespace = os.Getenv("PORTER_NAMESPACE")
  337. // next, check for values in the YAML file
  338. if target.Project == 0 {
  339. if project, ok := genericTarget["project"]; ok {
  340. projectVal, ok := project.(uint)
  341. if !ok {
  342. return nil, fmt.Errorf("project value must be an integer")
  343. }
  344. target.Project = projectVal
  345. }
  346. }
  347. if target.Cluster == 0 {
  348. if cluster, ok := genericTarget["cluster"]; ok {
  349. clusterVal, ok := cluster.(uint)
  350. if !ok {
  351. return nil, fmt.Errorf("cluster value must be an integer")
  352. }
  353. target.Cluster = clusterVal
  354. }
  355. }
  356. if target.Namespace == "" {
  357. if namespace, ok := genericTarget["namespace"]; ok {
  358. namespaceVal, ok := namespace.(string)
  359. if !ok {
  360. return nil, fmt.Errorf("invalid namespace provided")
  361. }
  362. target.Namespace = namespaceVal
  363. }
  364. }
  365. // lastly, just put in the defaults
  366. if target.Project == 0 {
  367. target.Project = config.Project
  368. }
  369. if target.Cluster == 0 {
  370. target.Cluster = config.Cluster
  371. }
  372. if target.Namespace == "" {
  373. target.Namespace = "default"
  374. }
  375. return target, nil
  376. }
  377. func getConfig(genericConfig map[string]interface{}) (*Config, error) {
  378. config := &Config{}
  379. err := mapstructure.Decode(genericConfig, config)
  380. if err != nil {
  381. return nil, err
  382. }
  383. return config, nil
  384. }