2
0

apply.go 9.1 KB

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