diff --git a/codefresh/cfclient/gitops_environments.go b/codefresh/cfclient/gitops_environments.go new file mode 100644 index 0000000..7cad1d9 --- /dev/null +++ b/codefresh/cfclient/gitops_environments.go @@ -0,0 +1,206 @@ +package cfclient + +import ( + "fmt" +) + +const ( + environmentQueryFields = ` + id + name + kind + clusters { + runtimeName + name + server + namespaces + } + labelPairs + ` +) + +// GitopsEnvironment represents a GitOps environment configuration. +type GitopsEnvironment struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Kind string `json:"kind"` + Clusters []GitopsEnvironmentCluster `json:"clusters"` + LabelPairs []string `json:"labelPairs"` +} + +// GitopsCluster represents a cluster within a GitOps environment. +type GitopsEnvironmentCluster struct { + Name string `json:"name"` + RuntimeName string `json:"runtimeName"` + Namespaces []string `json:"namespaces"` +} + +type GitopsEnvironmentResponse struct { + Errors []GraphQLError `json:"errors,omitempty"` + Data struct { + Environment GitopsEnvironment `json:"environment,omitempty"` + CreateEnvironment GitopsEnvironment `json:"createEnvironment,omitempty"` + UpdateEnvironment GitopsEnvironment `json:"updateEnvironment,omitempty"` + DeleteEnvironment GitopsEnvironment `json:"deleteEnvironment,omitempty"` + } `json:"data"` +} + +type GitopsEnvironmentsResponse struct { + Data struct { + Environments []GitopsEnvironment `json:"environments,omitempty"` + } `json:"data"` +} + +// At the time of writing Codefresh Graphql API does not support fetching environment by ID, So all environemnts are fetched and filtered within this client +func (client *Client) GetGitopsEnvironments() (*[]GitopsEnvironment, error) { + + request := GraphQLRequest{ + Query: fmt.Sprintf(` + query ($filters: EnvironmentFilterArgs) { + environments(filters: $filters) { + %s + } + } + `, environmentQueryFields), + Variables: map[string]interface{}{}, + } + + response, err := client.SendGqlRequest(request) + + if err != nil { + return nil, err + } + + var gitopsEnvironmentsResponse GitopsEnvironmentsResponse + err = DecodeGraphQLResponseInto(response, &gitopsEnvironmentsResponse) + + if err != nil { + return nil, err + } + + return &gitopsEnvironmentsResponse.Data.Environments, nil +} + +func (client *Client) GetGitopsEnvironmentById(id string) (*GitopsEnvironment, error) { + environments, err := client.GetGitopsEnvironments() + + if err != nil { + return nil, err + } + + for _, env := range *environments { + if env.ID == id { + return &env, nil + } + } + + return nil, nil +} + +func (client *Client) CreateGitopsEnvironment(environment *GitopsEnvironment) (*GitopsEnvironment, error) { + request := GraphQLRequest{ + Query: fmt.Sprintf(` + mutation ($environment: CreateEnvironmentArgs!) { + createEnvironment(environment: $environment) { + %s + } + } + `, environmentQueryFields), + Variables: map[string]interface{}{ + "environment": environment, + }, + } + + response, err := client.SendGqlRequest(request) + + if err != nil { + return nil, err + } + + var gitopsEnvironmentResponse GitopsEnvironmentResponse + err = DecodeGraphQLResponseInto(response, &gitopsEnvironmentResponse) + + if err != nil { + return nil, err + } + + if len(gitopsEnvironmentResponse.Errors) > 0 { + return nil, fmt.Errorf("CreateGitopsEnvironment - %s", gitopsEnvironmentResponse.Errors) + } + + return &gitopsEnvironmentResponse.Data.CreateEnvironment, nil +} + +func (client *Client) DeleteGitopsEnvironment(id string) (*GitopsEnvironment, error) { + + type deleteEnvironmentArgs struct { + ID string `json:"id"` + } + + request := GraphQLRequest{ + Query: fmt.Sprintf(` + mutation ($environment: DeleteEnvironmentArgs!) { + deleteEnvironment(environment: $environment) { + %s + } + } + `, environmentQueryFields), + + Variables: map[string]interface{}{ + "environment": deleteEnvironmentArgs{ + ID: id, + }, + }, + } + + response, err := client.SendGqlRequest(request) + + if err != nil { + return nil, err + } + var gitopsEnvironmentResponse GitopsEnvironmentResponse + err = DecodeGraphQLResponseInto(response, &gitopsEnvironmentResponse) + + if err != nil { + return nil, err + } + + if len(gitopsEnvironmentResponse.Errors) > 0 { + return nil, fmt.Errorf("DeleteGitopsEnvironment - %s", gitopsEnvironmentResponse.Errors) + } + + return &gitopsEnvironmentResponse.Data.DeleteEnvironment, nil +} + +func (client *Client) UpdateGitopsEnvironment(environment *GitopsEnvironment) (*GitopsEnvironment, error) { + request := GraphQLRequest{ + Query: fmt.Sprintf(` + mutation ($environment: UpdateEnvironmentArgs!) { + updateEnvironment(environment: $environment) { + %s + } + } + `, environmentQueryFields), + Variables: map[string]interface{}{ + "environment": environment, + }, + } + + response, err := client.SendGqlRequest(request) + + if err != nil { + return nil, err + } + var gitopsEnvironmentResponse GitopsEnvironmentResponse + err = DecodeGraphQLResponseInto(response, &gitopsEnvironmentResponse) + + if err != nil { + return nil, err + } + + if len(gitopsEnvironmentResponse.Errors) > 0 { + return nil, fmt.Errorf("UpdateGitopsEnvironment - %s", gitopsEnvironmentResponse.Errors) + } + + return &gitopsEnvironmentResponse.Data.UpdateEnvironment, nil +} diff --git a/codefresh/cfclient/gql_client.go b/codefresh/cfclient/gql_client.go index ce3d72d..a887668 100644 --- a/codefresh/cfclient/gql_client.go +++ b/codefresh/cfclient/gql_client.go @@ -14,6 +14,11 @@ type GraphQLRequest struct { Variables map[string]interface{} `json:"variables,omitempty"` } +type GraphQLError struct { + Message string `json:"message,omitempty"` + Extensions string `json:"extensions,omitempty"` +} + func (client *Client) SendGqlRequest(request GraphQLRequest) ([]byte, error) { jsonRequest, err := json.Marshal(request) if err != nil { diff --git a/codefresh/provider.go b/codefresh/provider.go index 39d2fbf..be0c132 100644 --- a/codefresh/provider.go +++ b/codefresh/provider.go @@ -76,6 +76,7 @@ func Provider() *schema.Provider { "codefresh_account_idp": resourceAccountIdp(), "codefresh_account_gitops_settings": resourceAccountGitopsSettings(), "codefresh_service_account": resourceServiceAccount(), + "codefresh_gitops_environment": resourceGitopsEnvironment(), }, ConfigureFunc: configureProvider, } diff --git a/codefresh/resource_gitops_environment.go b/codefresh/resource_gitops_environment.go new file mode 100644 index 0000000..1bcbd0d --- /dev/null +++ b/codefresh/resource_gitops_environment.go @@ -0,0 +1,214 @@ +package codefresh + +import ( + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/internal/datautil" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGitopsEnvironment() *schema.Resource { + return &schema.Resource{ + Description: "An environment in Codefresh GitOps is a logical grouping of one or more Kubernetes clusters and namespaces, representing a deployment context for your Argo CD applications. See [official documentation](https://codefresh.io/docs/gitops/environments/environments-overview/) for more information.", + Create: resourceGitopsEnvironmentCreate, + Read: resourceGitopsEnvironmentRead, + Update: resourceGitopsEnvironmentUpdate, + Delete: resourceGitopsEnvironmentDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "id": { + Description: "Environment ID", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the environment. Must be unique per account", + }, + "kind": { + Type: schema.TypeString, + Required: true, + Description: "The type of environment. Possible values: NON_PROD, PROD", + ValidateFunc: validation.StringInSlice([]string{"NON_PROD", "PROD"}, false), + }, + "cluster": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Target cluster name", + }, + "runtime_name": { + Type: schema.TypeString, + Required: true, + Description: "Runtime name where the target cluster is registered", + }, + "namespaces": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "List of namespaces in the target cluster", + }, + }, + }, + }, + "label_pairs": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "List of labels and values in the format label=value that can be used to assign applications to the environment. Example: ['codefresh.io/environment=prod']", + }, + }, + } +} + +// func resourceGitopsEnvironmentCreate(d *schema.ResourceData, m interface{}) error { +func resourceGitopsEnvironmentCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + environment := mapResourceToGitopsEnvironment(d) + newEnvironment, err := client.CreateGitopsEnvironment(environment) + + if err != nil { + return err + } + + d.SetId(newEnvironment.ID) + + return resourceGitopsEnvironmentRead(d, meta) +} + +func resourceGitopsEnvironmentUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + environment := mapResourceToGitopsEnvironment(d) + _, err := client.UpdateGitopsEnvironment(environment) + + if err != nil { + return err + } + + return resourceGitopsEnvironmentRead(d, meta) +} + +func resourceGitopsEnvironmentDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + id := d.Id() + if id == "" { + d.SetId("") + return nil + } + + _, err := client.DeleteGitopsEnvironment(id) + + if err != nil { + return err + } + + d.SetId("") + + return nil +} + +func resourceGitopsEnvironmentRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + id := d.Id() + if id == "" { + d.SetId("") + return nil + } + + environment, err := client.GetGitopsEnvironmentById(id) + + if err != nil { + return err + } + + if environment == nil { + d.SetId("") + return nil + } + + return mapGitopsEnvironmentToResource(d, environment) +} + +func mapResourceToGitopsEnvironment(d *schema.ResourceData) *cfclient.GitopsEnvironment { + + clusters := expandClusters(d.Get("cluster").([]interface{})) + + labelPairs := []string{} + + if len(d.Get("label_pairs").([]interface{})) > 0 { + labelPairs = datautil.ConvertStringArr(d.Get("label_pairs").([]interface{})) + } + + return &cfclient.GitopsEnvironment{ + ID: d.Get("id").(string), + Name: d.Get("name").(string), + Kind: d.Get("kind").(string), + Clusters: clusters, + LabelPairs: labelPairs, + } +} + +func mapGitopsEnvironmentToResource(d *schema.ResourceData, environment *cfclient.GitopsEnvironment) error { + if err := d.Set("id", environment.ID); err != nil { + return err + } + + if err := d.Set("name", environment.Name); err != nil { + return err + } + + if err := d.Set("kind", environment.Kind); err != nil { + return err + } + + if err := d.Set("cluster", flattenClusters(environment.Clusters)); err != nil { + return err + } + + if err := d.Set("label_pairs", environment.LabelPairs); err != nil { + return err + } + return nil +} + +func flattenClusters(clusters []cfclient.GitopsEnvironmentCluster) []map[string]interface{} { + + var res = make([]map[string]interface{}, 0) + + for _, cluster := range clusters { + m := make(map[string]interface{}) + m["name"] = cluster.Name + m["runtime_name"] = cluster.RuntimeName + m["namespaces"] = cluster.Namespaces + res = append(res, m) + } + + return res +} + +func expandClusters(list []interface{}) []cfclient.GitopsEnvironmentCluster { + var clusters = make([]cfclient.GitopsEnvironmentCluster, 0) + + for _, item := range list { + clusterMap := item.(map[string]interface{}) + cluster := cfclient.GitopsEnvironmentCluster{ + Name: clusterMap["name"].(string), + RuntimeName: clusterMap["runtime_name"].(string), + Namespaces: datautil.ConvertStringArr(clusterMap["namespaces"].([]interface{})), + } + clusters = append(clusters, cluster) + } + return clusters +} diff --git a/codefresh/resource_gitops_environment_test.go b/codefresh/resource_gitops_environment_test.go new file mode 100644 index 0000000..001b231 --- /dev/null +++ b/codefresh/resource_gitops_environment_test.go @@ -0,0 +1,127 @@ +package codefresh + +import ( + "fmt" + "strings" + "testing" + + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccCodefreshGitopsEnvironmentsResource(t *testing.T) { + resourceName := "codefresh_gitops_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCodefreshGitopsEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCodefreshGitopsEnvironmentConfig( + "test-for-tf", + "NON_PROD", + []cfclient.GitopsEnvironmentCluster{{ + Name: "in-cluster2", + RuntimeName: "test-runtime", + Namespaces: []string{"test-ns-1", "test-ns2"}, + }}, + []string{"codefresh.io/environment=test-for-tf", "codefresh.io/environment-1=test-for-tf1"}, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshGitopsEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "test-for-tf"), + resource.TestCheckResourceAttr(resourceName, "kind", "NON_PROD"), + resource.TestCheckResourceAttr(resourceName, "cluster.0.name", "in-cluster2"), + resource.TestCheckResourceAttr(resourceName, "cluster.0.runtime_name", "test-runtime"), + resource.TestCheckResourceAttr(resourceName, "cluster.0.namespaces.0", "test-ns-1"), + resource.TestCheckResourceAttr(resourceName, "cluster.0.namespaces.1", "test-ns2"), + resource.TestCheckResourceAttr(resourceName, "label_pairs.0", "codefresh.io/environment=test-for-tf"), + resource.TestCheckResourceAttr(resourceName, "label_pairs.1", "codefresh.io/environment-1=test-for-tf1"), + ), + }, + { + Config: testAccCodefreshGitopsEnvironmentConfig( + "test-for-tf", + "NON_PROD", + []cfclient.GitopsEnvironmentCluster{ + { + Name: "in-cluster2", + RuntimeName: "test-runtime", + Namespaces: []string{"test-ns-1", "test-ns2"}, + }, + { + Name: "in-cluster3", + RuntimeName: "test-runtime-2", + Namespaces: []string{"test-ns-3"}, + }, + }, + []string{"codefresh.io/environment=test-for-tf", "codefresh.io/environment-1=test-for-tf1"}, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshGitopsEnvironmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "test-for-tf"), + resource.TestCheckResourceAttr(resourceName, "kind", "NON_PROD"), + resource.TestCheckResourceAttr(resourceName, "cluster.0.name", "in-cluster2"), + resource.TestCheckResourceAttr(resourceName, "cluster.1.name", "in-cluster3"), + resource.TestCheckResourceAttr(resourceName, "cluster.1.runtime_name", "test-runtime-2"), + resource.TestCheckResourceAttr(resourceName, "cluster.1.namespaces.0", "test-ns-3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckCodefreshGitopsEnvironmentExists(resource string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Not found: %s", resource) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + envID := rs.Primary.ID + apiClient := testAccProvider.Meta().(*cfclient.Client) + _, err := apiClient.GetGitopsEnvironmentById(envID) + if err != nil { + return fmt.Errorf("error fetching gitops environment with ID %s. %s", envID, err) + } + return nil + } +} + +func testAccCheckCodefreshGitopsEnvironmentDestroy(state *terraform.State) error { + // Implement destroy check if needed + return nil +} + +// CONFIG +func testAccCodefreshGitopsEnvironmentConfig(name, kind string, clusters []cfclient.GitopsEnvironmentCluster, labelPairs []string) string { + var clusterBlocks []string + for _, c := range clusters { + ns := fmt.Sprintf("[\"%s\"]", strings.Join(c.Namespaces, "\", \"")) + block := fmt.Sprintf(` cluster { + name = "%s" + runtime_name = "%s" + namespaces = %s + }`, c.Name, c.RuntimeName, ns) + clusterBlocks = append(clusterBlocks, block) + } + labelsStr := fmt.Sprintf("[\"%s\"]", strings.Join(labelPairs, "\", \"")) + return fmt.Sprintf(` +resource "codefresh_gitops_environment" "test" { + name = "%s" + kind = "%s" +%s + label_pairs = %s +} +`, name, kind, strings.Join(clusterBlocks, "\n"), labelsStr) +} diff --git a/docs/resources/gitops_environment.md b/docs/resources/gitops_environment.md new file mode 100644 index 0000000..0e0a7eb --- /dev/null +++ b/docs/resources/gitops_environment.md @@ -0,0 +1,60 @@ +--- +page_title: "codefresh_gitops_environment Resource - terraform-provider-codefresh" +subcategory: "" +description: |- + An environment in Codefresh GitOps is a logical grouping of one or more Kubernetes clusters and namespaces, representing a deployment context for your Argo CD applications. See official documentation https://codefresh.io/docs/gitops/environments/environments-overview/ for more information. +--- + +# codefresh_gitops_environment (Resource) + +An environment in Codefresh GitOps is a logical grouping of one or more Kubernetes clusters and namespaces, representing a deployment context for your Argo CD applications. See [official documentation](https://codefresh.io/docs/gitops/environments/environments-overview/) for more information. + +## Example Usage + +```hcl +resource "codefresh_gitops_environment" "example" { + name = "test-gitops-env" + kind = "NON_PROD" + + cluster { + name = "test-cluster" + server = "https://kubernetes.default.svc" + runtime_name = "test-runtime" + namespaces = ["test-ns-1", "test-ns-2"] + } + + label_pairs = [ + "codefresh.io/environment=test-gitops-env", + "codefresh.io/environment-1=test-gitops-env1" + ] +} +``` + + +## Schema + +### Required + +- `cluster` (Block List, Min: 1) (see [below for nested schema](#nestedblock--cluster)) +- `kind` (String) The type of environment. Possible values: NON_PROD, PROD +- `name` (String) The name of the environment. Must be unique per account + +### Optional + +- `id` (String) Environment ID +- `label_pairs` (List of String) List of labels and values in the format label=value that can be used to assign applications to the environment. Example: ['codefresh.io/environment=prod'] + + +### Nested Schema for `cluster` + +Required: + +- `name` (String) Target cluster name +- `namespaces` (List of String) List of namespaces in the target cluster +- `runtime_name` (String) Runtime name where the target cluster is registered + +## Import + +```sh +terraform import codefresh_gitops_environment.example +``` diff --git a/templates/resources/gitops_environment.md.tmpl b/templates/resources/gitops_environment.md.tmpl new file mode 100644 index 0000000..12e08aa --- /dev/null +++ b/templates/resources/gitops_environment.md.tmpl @@ -0,0 +1,39 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- + {{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example Usage + +```hcl +resource "codefresh_gitops_environment" "example" { + name = "test-gitops-env" + kind = "NON_PROD" + + cluster { + name = "test-cluster" + server = "https://kubernetes.default.svc" + runtime_name = "test-runtime" + namespaces = ["test-ns-1", "test-ns-2"] + } + + label_pairs = [ + "codefresh.io/environment=test-gitops-env", + "codefresh.io/environment-1=test-gitops-env1" + ] +} +``` + +{{ .SchemaMarkdown | trimspace }} + +## Import + +```sh +terraform import codefresh_gitops_environment.example +```