Skip to content

Commit

Permalink
Add support for contexts via the unstable GraphQL API (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
bendrucker authored May 24, 2020
1 parent f672452 commit 697bdd7
Show file tree
Hide file tree
Showing 293 changed files with 60,947 additions and 36,769 deletions.
45 changes: 45 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
TEST?=$$(go list ./... |grep -v 'vendor')
GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor)
PKG_NAME=circleci

default: build

build: fmtcheck
go install

test: fmtcheck
go test -i $(TEST) || exit 1
echo $(TEST) | \
xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4

testacc: fmtcheck
TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 30m

vet:
@echo "go vet ."
@go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \
echo ""; \
echo "Vet found suspicious constructs. Please check the reported constructs"; \
echo "and fix them if necessary before submitting the code for review."; \
exit 1; \
fi

fmt:
gofmt -s -w $(GOFMT_FILES)

fmtcheck:
@sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'"

errcheck:
@sh -c "'$(CURDIR)/scripts/errcheck.sh'"

test-compile:
@if [ "$(TEST)" = "./..." ]; then \
echo "ERROR: Set TEST to a specific package. For example,"; \
echo " make test-compile TEST=./$(PKG_NAME)"; \
exit 1; \
fi
go test -c $(TEST) $(TESTARGS)


.PHONY: build test testacc vet fmt fmtcheck errcheck test-compile
22 changes: 0 additions & 22 deletions Makefile

This file was deleted.

7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@
## Using the provider

#### Download a release

Download the latest release for your OS from the [release page][release page]
and follow the instructions to [install third party plugins][third party plugins].

#### Build from sources
To build the project you can use `make all` which will:
- run the tests (`make test`)
- build the binary (`make build`)
- copy the binary to the [Terraform plugin directory][third party plugins] (`make install_plugin_locally`)

To build the project you can use `make`. This will place a binary in your `$GOBIN` directory. Copy the binary to the [Terraform plugin directory][third party plugins].

After placing it into your plugins directory, run `terraform init` to initialize it.

Expand Down
130 changes: 130 additions & 0 deletions circleci/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package circleci

import (
"errors"
"fmt"
"net/url"

graphqlclient "github.com/CircleCI-Public/circleci-cli/client"
restclient "github.com/jszwedko/go-circleci"
)

// Client provides access to the CircleCI REST and GraphQL APIs
type Client struct {
rest *restclient.Client
graphql *graphqlclient.Client
vcs string
organization string
}

// Config configures a Client
type Config struct {
URL string
GraphqlURL string
Token string

VCS string
Organization string
}

// NewClient initializes CircleCI API clients (REST and GraphQL) and returns a new client object
func NewClient(config Config) (*Client, error) {
restURL, err := url.Parse(config.URL)
if err != nil {
return nil, err
}

graphqlURL, err := url.Parse(config.GraphqlURL)
if err != nil {
return nil, err
}

return &Client{
rest: &restclient.Client{
BaseURL: restURL,
Token: config.Token,
},
graphql: graphqlclient.NewClient(
fmt.Sprintf("%s://%s", graphqlURL.Scheme, graphqlURL.Host),
graphqlURL.Path,
config.Token,
false,
),

vcs: config.VCS,
organization: config.Organization,
}, nil
}

// Organization returns the organization for a request. If an organization is provided,
// that is returned. Next, an organization configured in the provider is returned.
// If neither are set, an error is returned.
func (c *Client) Organization(org string) (string, error) {
if org != "" {
return org, nil
}

if c.organization != "" {
return c.organization, nil
}

return "", errors.New("organization is required")
}

// GetEnvVar get the environment variable with given name
// It returns an empty structure if no environment variable exists with that name
func (c *Client) GetEnvVar(organization string, projectName, envVarName string) (*restclient.EnvVar, error) {
org, err := c.validateOrganization(organization, projectName, envVarName)
if err != nil {
return nil, err
}

return c.rest.GetEnvVar(c.vcs, org, projectName, envVarName)
}

// EnvVarExists check if environment variable exists with given name
func (c *Client) EnvVarExists(organization string, projectName, envVarName string) (bool, error) {
org, err := c.validateOrganization(organization, projectName, envVarName)
if err != nil {
return false, err
}

envVar, err := c.rest.GetEnvVar(c.vcs, org, projectName, envVarName)
if err != nil {
return false, err
}
return bool(envVar.Name != ""), nil
}

// AddEnvVar create an environment variable with given name and value
func (c *Client) AddEnvVar(organization string, projectName, envVarName, envVarValue string) (*restclient.EnvVar, error) {
org, err := c.validateOrganization(organization, projectName, envVarName)
if err != nil {
return nil, err
}

return c.rest.AddEnvVar(c.vcs, org, projectName, envVarName, envVarValue)
}

// DeleteEnvVar delete the environment variable with given name
func (c *Client) DeleteEnvVar(organization string, projectName, envVarName string) error {
org, err := c.validateOrganization(organization, projectName, envVarName)
if err != nil {
return err
}

return c.rest.DeleteEnvVar(c.vcs, org, projectName, envVarName)
}

func (c *Client) validateOrganization(organization string, projectName, envVarName string) (string, error) {
if organization == "" && c.organization == "" {
return "", fmt.Errorf("organization has not been set for environment variable %s in project %s", projectName, envVarName)
}

if organization != "" {
return organization, nil
}

return c.organization, nil

}
66 changes: 66 additions & 0 deletions circleci/client_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package circleci

import (
"errors"
"fmt"

"github.com/CircleCI-Public/circleci-cli/api"
graphql "github.com/CircleCI-Public/circleci-cli/client"
)

var (
ErrContextNotFound = errors.New("context not found")
)

// GetContextByIDOrName lists all contexts and returns one with a matching ID or name, if found
func GetContextByIDOrName(client *graphql.Client, org string, vcs string, value string) (*api.CircleCIContext, error) {
ctx, err := GetContextByID(client, org, vcs, value)
if err != nil && !errors.Is(err, ErrContextNotFound) {
return nil, err
}
if ctx != nil {
return ctx, nil
}

ctx, err = GetContextByName(client, org, vcs, value)
if err != nil && !errors.Is(err, ErrContextNotFound) {
return nil, err
}
if ctx != nil {
return ctx, nil
}

return nil, fmt.Errorf("%w: no context with ID or name '%s' in organization '%s'", ErrContextNotFound, value, org)
}

// GetContextByName lists all contexts and returns one with a matching name, if found
func GetContextByName(client *graphql.Client, org string, vcs string, name string) (*api.CircleCIContext, error) {
res, err := api.ListContexts(client, org, vcs)
if err != nil {
return nil, err
}

for _, context := range res.Organization.Contexts.Edges {
if context.Node.Name == name {
return &context.Node, nil
}
}

return nil, fmt.Errorf("%w: no context with name '%s' in organization '%s'", ErrContextNotFound, name, org)
}

// GetContextByID lists all contexts and returns one with a matching ID, if found
func GetContextByID(client *graphql.Client, org string, vcs string, ID string) (*api.CircleCIContext, error) {
res, err := api.ListContexts(client, org, vcs)
if err != nil {
return nil, err
}

for _, context := range res.Organization.Contexts.Edges {
if context.Node.ID == ID {
return &context.Node, nil
}
}

return nil, fmt.Errorf("%w: no context with ID '%s' in organization '%s'", ErrContextNotFound, ID, org)
}
54 changes: 54 additions & 0 deletions circleci/data_source_circleci_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package circleci

import (
"errors"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func dataSourceCircleCIContext() *schema.Resource {
return &schema.Resource{
Read: dataSourceCircleCIContextRead,

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
Description: "The name of the context",
},
"organization": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Description: "The organization where the context is defined",
},
},
}
}

func dataSourceCircleCIContextRead(d *schema.ResourceData, m interface{}) error {
client := m.(*Client)

org, err := client.Organization(d.Get("organization").(string))
if err != nil {
return err
}

ctx, err := GetContextByName(
client.graphql,
org,
client.vcs,
d.Get("name").(string),
)
if err != nil {
if errors.Is(err, ErrContextNotFound) {
d.SetId("")
return nil
}

return err
}

d.SetId(ctx.ID)
return nil
}
32 changes: 32 additions & 0 deletions circleci/data_source_circleci_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package circleci

import (
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccCircleCIContextDataSource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccOrgProviders,
Steps: []resource.TestStep{
{
Config: testAccCircleCIContextDataSource,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.circleci_context.foo", "name", "terraform-test"),
),
},
},
})
}

const testAccCircleCIContextDataSource = `
resource "circleci_context" "foo" {
name = "terraform-test"
}
data "circleci_context" "foo" {
name = circleci_context.foo.name
}
`
Loading

0 comments on commit 697bdd7

Please sign in to comment.