Last active
July 10, 2020 21:00
-
-
Save battlecow/54676f251aabe9cfda962a04e1dcef3a to your computer and use it in GitHub Desktop.
This file contains hidden or 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
package spinnaker | |
import ( | |
"bytes" | |
"encoding/json" | |
"fmt" | |
"io" | |
"log" | |
"net/http" | |
) | |
type errorResponse struct { | |
Timestamp uint | |
Status int | |
Error string | |
Message string | |
} | |
type XraySegment interface { | |
AddAnnotation(key string, value interface{}) error | |
AddMetadata(key string, value interface{}) error | |
Close(err error) | |
} | |
type HttpClient interface { | |
Get(path string) (*http.Response, error) | |
Post(path string, body io.Reader) (*http.Response, error) | |
Put(path string, body io.Reader) (*http.Response, error) | |
Patch(path string, body io.Reader) (*http.Response, error) | |
Delete(path string) (*http.Response, error) | |
AddAnnotation(key string, value interface{}) () | |
BeginSubsegment(name string) XraySegment | |
} | |
type Client struct { | |
httpClient HttpClient | |
} | |
// Factory function to create a new Spinnaker client | |
func NewClient(httpClient HttpClient) *Client { | |
client := &Client{ | |
httpClient: httpClient, | |
} | |
return client | |
} | |
func (c *Client) GetApplications() ([]Application, error) { | |
resp, err := c.httpClient.Get("applications") | |
if err != nil { | |
if resp == nil { | |
return nil, &PipelineTimeoutError{status: 0, msg: fmt.Sprintf("unable to retrieve applications request timed out: %s", err.Error())} | |
} | |
return nil, &PipelineGenericError{status: resp.StatusCode, msg: fmt.Sprintf("unable to retrieve applications: %s", err.Error())} | |
} | |
dec := json.NewDecoder(resp.Body) | |
if resp.StatusCode != 200 { | |
var errorResponse errorResponse | |
err = dec.Decode(&errorResponse) | |
if err != nil { | |
log.Println(err) | |
} | |
switch resp.StatusCode { | |
case 404: | |
return nil, &PipelineNotFoundError{status: resp.StatusCode, msg: errorResponse.Message} | |
default: | |
return nil, &PipelineGenericError{status: resp.StatusCode, msg: "unable to get applications"} | |
} | |
} | |
var applicationData []Application | |
err = dec.Decode(&applicationData) | |
if err != nil { | |
log.Println(err) | |
return nil, err | |
} | |
return applicationData, nil | |
} | |
// StartPipeline takes applicationName, pipelineName, and a trigger struct and begins a pipeline run | |
// returns the pipeline id if successful or an error | |
func (c *Client) StartPipeline(applicationName string, pipelineName string, trigger Trigger) (string, error) { | |
var pipelineResponse PipelineResponse | |
// Xray requires a subsegment to be created prior to adding annotations or metadata when using lambda: https://github.com/aws/aws-xray-sdk-go/issues/57 | |
subSeg := c.httpClient.BeginSubsegment("startPipeline") | |
defer subSeg.Close(nil) | |
c.httpClient.AddAnnotation("application", applicationName) | |
c.httpClient.AddAnnotation("pipelineName", pipelineName) | |
// Parse defined triggers (artifacts/parameters) | |
triggerData, err := json.Marshal(trigger) | |
if err != nil { | |
log.Println(err) | |
} | |
body := bytes.NewBuffer(triggerData) | |
resp, err := c.httpClient.Post(fmt.Sprintf("pipelines/%s/%s", applicationName, pipelineName), body) | |
if err != nil { | |
if resp == nil { | |
return "", &PipelineTimeoutError{status: 0, msg: fmt.Sprintf("unable to start pipeline request timed out: %s", err.Error())} | |
} | |
return "", &PipelineGenericError{status: resp.StatusCode, msg: fmt.Sprintf("unable to start pipeline: %s", err.Error())} | |
} | |
dec := json.NewDecoder(resp.Body) | |
if resp.StatusCode != 201 && resp.StatusCode != 202 { | |
var errorResponse errorResponse | |
err = dec.Decode(&errorResponse) | |
if err != nil { | |
log.Println(err) | |
} | |
switch resp.StatusCode { | |
case 404: | |
return "", &PipelineNotFoundError{status: resp.StatusCode, msg: errorResponse.Message} | |
case 405: | |
return "", &PipelineMethodNotAllowedError{status: resp.StatusCode, msg: errorResponse.Message} | |
default: | |
return "", &PipelineGenericError{status: resp.StatusCode, msg: "unable to start pipeline"} | |
} | |
} | |
err = dec.Decode(&pipelineResponse) | |
if err != nil { | |
log.Println(err) | |
return "", err | |
} | |
pipeline := &Pipeline{} | |
pipelineId := pipeline.ParsePipelineResponse(&pipelineResponse) | |
c.httpClient.AddAnnotation("pipelineId", pipelineId) | |
return pipelineId, nil | |
} | |
// GetPipelineById retrieves a single pipeline by id | |
func (c *Client) GetPipelineById(pipelineId string) (Pipeline, error) { | |
var pipeline Pipeline | |
resp, err := c.httpClient.Get(fmt.Sprintf("pipelines/%s", pipelineId)) | |
if err != nil { | |
if resp == nil { | |
return pipeline, &PipelineTimeoutError{status: 0, msg: fmt.Sprintf("unable to retrieve pipeline request timed out: %s", err.Error())} | |
} | |
return pipeline, &PipelineGenericError{status: resp.StatusCode, msg: fmt.Sprintf("unable to retrieve pipeline: %s", err.Error())} | |
} | |
dec := json.NewDecoder(resp.Body) | |
if resp.StatusCode != 200 { | |
var errorResponse errorResponse | |
err = dec.Decode(&errorResponse) | |
if err != nil { | |
log.Println(err) | |
} | |
switch resp.StatusCode { | |
case 404: | |
return pipeline, &PipelineNotFoundError{status: resp.StatusCode, msg: errorResponse.Message} | |
case 405: | |
return pipeline, &PipelineMethodNotAllowedError{status: resp.StatusCode, msg: errorResponse.Message} | |
default: | |
return pipeline, &PipelineGenericError{status: resp.StatusCode, msg: "unable to retrieve pipeline"} | |
} | |
} | |
err = dec.Decode(&pipeline) | |
if err != nil { | |
log.Println(err) | |
return pipeline, err | |
} | |
return pipeline, nil | |
} | |
// GetPipelineStatus returns the specific Status of the pipeline | |
func (c *Client) GetPipelineStatus(pipelineId string) (StatusType, error) { | |
var err error | |
subSeg := c.httpClient.BeginSubsegment("getPipelineStatus") | |
defer subSeg.Close(nil) | |
pipeline, err := c.GetPipelineById(pipelineId) | |
if err != nil { | |
return "", err | |
} | |
c.httpClient.AddAnnotation("application", pipeline.Application) | |
c.httpClient.AddAnnotation("pipelineName", pipeline.Name) | |
c.httpClient.AddAnnotation("pipelineStatus", string(pipeline.Status)) | |
return pipeline.Status, nil | |
} |
This file contains hidden or 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
package spinnaker | |
import ( | |
"context" | |
"crypto/tls" | |
"crypto/x509" | |
"errors" | |
"flag" | |
"fmt" | |
"github.com/aws/aws-xray-sdk-go/xray" | |
"io" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"os" | |
"time" | |
) | |
const ( | |
defaultHTTPTimeout = 30 * time.Second | |
baseURL = "https://spinnaker-services-gate-api" | |
contentTypeKey = "Content-Type" | |
contentTypeValue = "application/json" | |
) | |
type httpClient struct { | |
client *http.Client | |
context context.Context | |
} | |
// Create the certificate chain for x509 authentication to Spinnaker | |
func createCertificateChain(clientCertPath string, clientKeyPath string, caCertPath string) (cert *tls.Certificate, caCertPool *x509.CertPool, err error) { | |
// Get the Orion service client certificate | |
clientCert, err := ioutil.ReadFile(clientCertPath) | |
if err != nil { | |
return nil, nil, fmt.Errorf("%w", errors.New("service certificate is missing")) | |
} | |
// Get the Orion service client key | |
clientKey, err := ioutil.ReadFile(clientKeyPath) | |
if err != nil { | |
return nil, nil, fmt.Errorf("%w", errors.New("service key is missing")) | |
} | |
// Create the key cert pair for TLS config | |
tlsCert, err := tls.X509KeyPair(clientCert, clientKey) | |
cert = &tlsCert | |
if err != nil { | |
return nil, nil, fmt.Errorf("%w", errors.New("error creating x509 keypair")) | |
} | |
// Load CA cert | |
caCert, err := ioutil.ReadFile(caCertPath) | |
if err != nil { | |
return nil, nil, fmt.Errorf("%w", errors.New("service certificate authority is missing")) | |
} | |
caCertPool = x509.NewCertPool() | |
caCertPool.AppendCertsFromPEM(caCert) | |
return cert, caCertPool, nil | |
} | |
// Function to register a single string variable if it is not already registered | |
func regStringVar(p *string, name string, value string, usage string) { | |
if flag.Lookup(name) == nil { | |
flag.StringVar(p, name, value, usage) | |
} | |
} | |
// Function to get the value of the flag after parsing | |
func getStringFlag(name string) string { | |
return flag.Lookup(name).Value.(flag.Getter).Get().(string) | |
} | |
// Function to check if the file exists on the filesystem | |
func fileExists(filename string) bool { | |
info, err := os.Stat(filename) | |
if os.IsNotExist(err) { | |
return false | |
} | |
return !info.IsDir() | |
} | |
// Read the certificate flags from the command line | |
// validate the file exists before proceeding | |
func readCertificateFlags() (clientCertPath string, clientKeyPath string, caCertPath string) { | |
flag.Parse() | |
clientCertPath = getStringFlag("cert") | |
clientKeyPath = getStringFlag("key") | |
caCertPath = getStringFlag("ca") | |
if !fileExists(clientCertPath) { | |
log.Printf("Fatal error: Certificate file not found: %s", clientCertPath) | |
flag.PrintDefaults() | |
os.Exit(1) | |
} | |
if !fileExists(clientKeyPath) { | |
log.Printf("Fatal error: Key file not found: %s", clientKeyPath) | |
flag.PrintDefaults() | |
os.Exit(1) | |
} | |
if !fileExists(caCertPath) { | |
log.Printf("Fatal error: Certificate Authority file not found: %s", caCertPath) | |
flag.PrintDefaults() | |
os.Exit(1) | |
} | |
return clientCertPath, clientKeyPath, caCertPath | |
} | |
// Flags get registered here with sane defaults where appropriate | |
func registerFlags() { | |
var clientCertPath, clientKeyPath, caCertPath string | |
regStringVar(&clientCertPath, "cert", "/opt/orion-service.crt", "Service API certificate") | |
regStringVar(&clientKeyPath, "key", "/opt/orion-service.key", "Service API key") | |
regStringVar(&caCertPath, "ca", "/opt/ca.crt", "Spinnaker CA certificate") | |
} | |
// Factory function to create a new Spinnaker low level HTTP client | |
func NewHttpClient(ctx context.Context) (HttpClient, error) { | |
registerFlags() | |
cert, caCertPool, err := createCertificateChain(readCertificateFlags()) | |
if err != nil { | |
return nil, err | |
} | |
newClient := &httpClient{} | |
// Setup the httpClient with a default timeout and the client cert, key, and self signed Spinnaker ca | |
client := &http.Client{ | |
Timeout: defaultHTTPTimeout, | |
Transport: &http.Transport{ | |
TLSClientConfig: &tls.Config{ | |
Certificates: []tls.Certificate{*cert}, | |
RootCAs: caCertPool, | |
}, | |
}, | |
} | |
// Attach the context to the client for xray use elsewhere | |
newClient.context = ctx | |
// Wrap the httpClient with xray | |
newClient.client = xray.Client(client) | |
return newClient, nil | |
} | |
// Get makes a HTTP GET request to provided path | |
func (c *httpClient) Get(path string) (*http.Response, error) { | |
var response *http.Response | |
xray.AddAnnotation(c.context, "get", path) | |
request, err := http.NewRequestWithContext(c.context, http.MethodGet, baseURL+"/"+path, nil) | |
if err != nil { | |
return response, err | |
} | |
headers := http.Header{} | |
request.Header = headers | |
return c.client.Do(request) | |
} | |
// Post makes a HTTP POST request to provided path and requestBody | |
func (c *httpClient) Post(path string, body io.Reader) (*http.Response, error) { | |
var response *http.Response | |
xray.AddAnnotation(c.context, "post", path) | |
request, err := http.NewRequestWithContext(c.context, http.MethodPost, baseURL+"/"+path, body) | |
if err != nil { | |
return response, err | |
} | |
headers := http.Header{} | |
headers.Set(contentTypeKey, contentTypeValue) | |
request.Header = headers | |
return c.client.Do(request) | |
} | |
// Put makes a HTTP PUT request to provided path and requestBody | |
func (c *httpClient) Put(path string, body io.Reader) (*http.Response, error) { | |
var response *http.Response | |
xray.AddAnnotation(c.context, "put", path) | |
request, err := http.NewRequestWithContext(c.context, http.MethodPut, baseURL+"/"+path, body) | |
if err != nil { | |
return response, err | |
} | |
headers := http.Header{} | |
headers.Set(contentTypeKey, contentTypeValue) | |
request.Header = headers | |
return c.client.Do(request) | |
} | |
// Patch makes a HTTP PATCH request to provided path and requestBody | |
func (c *httpClient) Patch(path string, body io.Reader) (*http.Response, error) { | |
var response *http.Response | |
xray.AddAnnotation(c.context, "patch", path) | |
request, err := http.NewRequestWithContext(c.context, http.MethodPatch, baseURL+"/"+path, body) | |
if err != nil { | |
return response, err | |
} | |
headers := http.Header{} | |
headers.Set(contentTypeKey, contentTypeValue) | |
request.Header = headers | |
return c.client.Do(request) | |
} | |
// Delete makes a HTTP DELETE request with provided path | |
func (c *httpClient) Delete(path string) (*http.Response, error) { | |
var response *http.Response | |
xray.AddAnnotation(c.context, "delete", path) | |
request, err := http.NewRequestWithContext(c.context, http.MethodDelete, baseURL+"/"+path, nil) | |
if err != nil { | |
return response, err | |
} | |
headers := http.Header{} | |
headers.Set("Content-Type", contentTypeValue) | |
request.Header = headers | |
return c.client.Do(request) | |
} | |
func (c *httpClient) AddAnnotation(key string, value interface{}) () { | |
xray.AddAnnotation(c.context, key, value.(string)) | |
} | |
func (c *httpClient) BeginSubsegment(name string) XraySegment { | |
var subSeg *xray.Segment | |
c.context, subSeg = xray.BeginSubsegment(c.context, name) | |
return subSeg | |
} |
This file contains hidden or 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
package main | |
import ( | |
"context" | |
"errors" | |
"fmt" | |
"github.com/aws/aws-lambda-go/lambda" | |
"log" | |
"jamf.com/orion/internal/spinnaker" | |
) | |
type pipelineEvent struct { | |
ApplicationName string `json:"application_name"` | |
PipelineName string `json:"pipeline_name"` | |
PipelineId string `json:"id"` | |
Type string `json:"type"` | |
} | |
type SpinnakerClient interface { | |
StartPipeline(applicationName string, pipelineName string, trigger spinnaker.Trigger) (string, error) | |
} | |
type deps struct { | |
spinClient SpinnakerClient | |
httpClient spinnaker.HttpClient | |
} | |
func (d *deps) handleRequest(ctx context.Context, event pipelineEvent) (pipelineEvent, error) { | |
if d.spinClient == nil { | |
log.Printf("Spinclient not defined, creating httpClient") | |
httpClient, err := spinnaker.NewHttpClient(ctx) | |
if err != nil { | |
log.Printf("Error creating spinnaker client: %s", err) | |
} | |
d.httpClient = httpClient | |
log.Printf("Creating spinnakerClient") | |
d.spinClient = spinnaker.NewClient(d.httpClient) | |
} | |
trigger := spinnaker.Trigger{} | |
log.Printf("Starting pipeline") | |
pipelineId, err := d.spinClient.StartPipeline(event.ApplicationName, event.PipelineName, trigger) | |
if err != nil { | |
var notFound *spinnaker.PipelineNotFoundError | |
if errors.As(err, ¬Found) { | |
log.Printf("Error: %s", err) | |
return event, err | |
} else { | |
log.Printf("Error starting pipeline: %s", err) | |
return event, err | |
} | |
} | |
log.Printf("Pipeline Id: %s\n", pipelineId) | |
event.PipelineId = pipelineId | |
event.Type = "pipeline" | |
d.spinClient = nil | |
return event, nil | |
} | |
func main() { | |
fmt.Println("Main Called") | |
d := deps{} | |
lambda.Start(d.handleRequest) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment