-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a provider for scheduled pipelines
This works structurally mostly like environment variables, but uses schedule IDs as internal IDs. Some hackery has been performed to make organization ID inheritance from provider settings work. It does work correctly for the most part, though local state can get a bit confused if the provider setting gets changed. Explicit organizations on schedules work just fine though. The scheduled actor ID in this is one of several magic IDs that we have at CircleCI, and I can guarantee to remain stable. CRUD operations have been verified to work off the local tree, and import of existing schedules works as well. Some provider-side validation is being performed, though it's much easier to just let the operation fail and print out the API error message, rather than duplicating all validation we perform in the API here. An example here is the project<>schedule-name uniqueness constraint, which is not checked in the provider. Similarly the requirement for either a branch or tag to be set as part of parameters, which is actually due to change soon.
- Loading branch information
Showing
6 changed files
with
548 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package client | ||
|
||
import ( | ||
"github.com/CircleCI-Public/circleci-cli/api" | ||
) | ||
|
||
func (c *Client) GetSchedule(id string) (*api.Schedule, error) { | ||
return c.schedules.ScheduleByID(id) | ||
} | ||
|
||
func (c *Client) CreateSchedule(organization, project, name, description string, timetable api.Timetable, useSchedulingSystem bool, parameters map[string]string) (*api.Schedule, error) { | ||
return c.schedules.CreateSchedule(c.vcs, organization, project, name, description, useSchedulingSystem, timetable, parameters) | ||
} | ||
|
||
func (c *Client) DeleteSchedule(id string) error { | ||
return c.schedules.DeleteSchedule(id) | ||
} | ||
|
||
func (c *Client) UpdateSchedule(id, name, description string, timetable api.Timetable, useSchedulingActor bool, parameters map[string]string) (*api.Schedule, error) { | ||
return c.schedules.UpdateSchedule(id, name, description, useSchedulingActor, timetable, parameters) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
package circleci | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/CircleCI-Public/circleci-cli/api" | ||
"github.com/hashicorp/terraform-plugin-sdk/helper/schema" | ||
|
||
client "github.com/mrolla/terraform-provider-circleci/circleci/client" | ||
) | ||
|
||
// NB Magic scheduled actor ID | ||
const scheduledActorID = "d9b3fcaa-6032-405a-8c75-40079ce33c3e" | ||
|
||
func resourceCircleCISchedule() *schema.Resource { | ||
return &schema.Resource{ | ||
Create: resourceCircleCIScheduleCreate, | ||
Read: resourceCircleCIScheduleRead, | ||
Delete: resourceCircleCIScheduleDelete, | ||
Update: resourceCircleCIScheduleUpdate, | ||
Importer: &schema.ResourceImporter{ | ||
State: resourceCircleCIScheduleImport, | ||
}, | ||
Schema: map[string]*schema.Schema{ | ||
"organization": { | ||
Type: schema.TypeString, | ||
Description: "The organization where the schedule will be created", | ||
Optional: true, | ||
ForceNew: true, | ||
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { | ||
return old == d.Get("organization").(string) | ||
}, | ||
}, | ||
"project": { | ||
Type: schema.TypeString, | ||
Description: "The name of the CircleCI project to create the schedule in", | ||
Required: true, | ||
ForceNew: true, | ||
}, | ||
"name": { | ||
Type: schema.TypeString, | ||
Description: "The name of the schedule", | ||
Required: true, | ||
}, | ||
"description": { | ||
Type: schema.TypeString, | ||
Description: "The description of the schedule", | ||
Optional: true, | ||
}, | ||
"per_hour": { | ||
Type: schema.TypeInt, | ||
Description: "How often per hour to trigger a pipeline", | ||
Required: true, | ||
}, | ||
"hours_of_day": { | ||
Type: schema.TypeList, | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeInt, | ||
}, | ||
Description: "Which hours of the day to trigger a pipeline", | ||
Required: true, | ||
}, | ||
"days_of_week": { | ||
Type: schema.TypeList, | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeString, | ||
}, | ||
Description: "Which days of the week (\"MON\" .. \"SUN\") to trigger a pipeline on", | ||
Required: true, | ||
}, | ||
"use_scheduling_system": { | ||
Type: schema.TypeBool, | ||
Description: "Use the scheduled system actor for attribution", | ||
Required: true, | ||
}, | ||
"parameters": { | ||
Type: schema.TypeMap, | ||
Description: "Pipeline parameters to pass to created pipelines", | ||
Optional: true, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func resourceCircleCIScheduleCreate(d *schema.ResourceData, m interface{}) error { | ||
c := m.(*client.Client) | ||
|
||
organization, err := c.Organization(d.Get("organization").(string)) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
project := d.Get("project").(string) | ||
name := d.Get("name").(string) | ||
description := d.Get("description").(string) | ||
useSchedulingSystem := d.Get("use_scheduling_system").(bool) | ||
|
||
parsedHours := d.Get("hours_of_day").([]interface{}) | ||
var hoursOfDay []uint | ||
for _, hour := range parsedHours { | ||
hoursOfDay = append(hoursOfDay, uint(hour.(int))) | ||
} | ||
|
||
var exists = struct{}{} | ||
validDays := make(map[string]interface{}) | ||
validDays["MON"] = exists | ||
validDays["TUE"] = exists | ||
validDays["WED"] = exists | ||
validDays["THU"] = exists | ||
validDays["FRI"] = exists | ||
validDays["SAT"] = exists | ||
validDays["SUN"] = exists | ||
|
||
parsedDays := d.Get("days_of_week").([]interface{}) | ||
var daysOfWeek []string | ||
for _, day := range parsedDays { | ||
if validDays[day.(string)] == nil { | ||
return fmt.Errorf("Invalid day specified: %s", day) | ||
} | ||
daysOfWeek = append(daysOfWeek, day.(string)) | ||
} | ||
|
||
timetable := api.Timetable{ | ||
PerHour: uint(d.Get("per_hour").(int)), | ||
HoursOfDay: hoursOfDay, | ||
DaysOfWeek: daysOfWeek, | ||
} | ||
|
||
parsedParams := d.Get("parameters").(map[string]interface{}) | ||
parameters := make(map[string]string) | ||
for k, v := range parsedParams { | ||
parameters[k] = v.(string) | ||
} | ||
|
||
schedule, err := c.CreateSchedule(organization, project, name, description, timetable, useSchedulingSystem, parameters) | ||
if err != nil { | ||
return fmt.Errorf("Failed to create schedule: %w", err) | ||
} | ||
|
||
d.SetId(schedule.ID) | ||
|
||
return resourceCircleCIScheduleRead(d, m) | ||
} | ||
|
||
func resourceCircleCIScheduleDelete(d *schema.ResourceData, m interface{}) error { | ||
c := m.(*client.Client) | ||
|
||
if err := c.DeleteSchedule(d.Id()); err != nil { | ||
return err | ||
} | ||
|
||
d.SetId("") | ||
|
||
return nil | ||
} | ||
|
||
func resourceCircleCIScheduleRead(d *schema.ResourceData, m interface{}) error { | ||
c := m.(*client.Client) | ||
id := d.Id() | ||
|
||
schedule, err := c.GetSchedule(id) | ||
if err != nil { | ||
return fmt.Errorf("Failed to read schedule: %s", id) | ||
} | ||
|
||
if schedule == nil { | ||
d.SetId("") | ||
return nil | ||
} | ||
|
||
_, organization, project, err := explodeProjectSlug(schedule.ProjectSlug) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
d.Set("organization", organization) | ||
d.Set("project", project) | ||
d.Set("name", schedule.Name) | ||
d.Set("description", schedule.Description) | ||
d.Set("per_hour", schedule.Timetable.PerHour) | ||
d.Set("hours_of_day", schedule.Timetable.HoursOfDay) | ||
d.Set("days_of_week", schedule.Timetable.DaysOfWeek) | ||
d.Set("parameters", schedule.Parameters) | ||
|
||
if schedule.Actor.ID == scheduledActorID { | ||
d.Set("use_scheduling_system", true) | ||
} else { | ||
d.Set("use_scheduling_system", false) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func resourceCircleCIScheduleUpdate(d *schema.ResourceData, m interface{}) error { | ||
c := m.(*client.Client) | ||
|
||
id := d.Id() | ||
name := d.Get("name").(string) | ||
description := d.Get("description").(string) | ||
attributionActor := d.Get("use_scheduling_system").(bool) | ||
|
||
parsedHours := d.Get("hours_of_day").([]interface{}) | ||
var hoursOfDay []uint | ||
for _, hour := range parsedHours { | ||
hoursOfDay = append(hoursOfDay, uint(hour.(int))) | ||
} | ||
|
||
var exists = struct{}{} | ||
validDays := make(map[string]interface{}) | ||
validDays["MON"] = exists | ||
validDays["TUE"] = exists | ||
validDays["WED"] = exists | ||
validDays["THU"] = exists | ||
validDays["FRI"] = exists | ||
validDays["SAT"] = exists | ||
validDays["SUN"] = exists | ||
|
||
parsedDays := d.Get("days_of_week").([]interface{}) | ||
var daysOfWeek []string | ||
for _, day := range parsedDays { | ||
if validDays[day.(string)] == nil { | ||
return fmt.Errorf("Invalid day specified: %s", day) | ||
} | ||
daysOfWeek = append(daysOfWeek, day.(string)) | ||
} | ||
|
||
timetable := api.Timetable{ | ||
PerHour: uint(d.Get("per_hour").(int)), | ||
HoursOfDay: hoursOfDay, | ||
DaysOfWeek: daysOfWeek, | ||
} | ||
|
||
parsedParams := d.Get("parameters").(map[string]interface{}) | ||
parameters := make(map[string]string) | ||
for k, v := range parsedParams { | ||
parameters[k] = v.(string) | ||
} | ||
|
||
_, err := c.UpdateSchedule(id, name, description, timetable, attributionActor, parameters) | ||
if err != nil { | ||
return fmt.Errorf("Failed to update schedule: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func resourceCircleCIScheduleImport(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { | ||
c := m.(*client.Client) | ||
|
||
schedule, err := c.GetSchedule(d.Id()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
d.SetId(schedule.ID) | ||
|
||
return []*schema.ResourceData{d}, nil | ||
} | ||
|
||
func explodeProjectSlug(slug string) (string, string, string, error) { | ||
matches := strings.Split(slug, "/") | ||
|
||
if len(matches) != 3 { | ||
return "", "", "", fmt.Errorf("Splitting project-slug '%s' into vcs/org/project failed", slug) | ||
} | ||
return matches[0], matches[1], matches[2], nil | ||
} |
Oops, something went wrong.