Skip to content

Instantly share code, notes, and snippets.

@battlecow
Last active July 10, 2020 21:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save battlecow/54676f251aabe9cfda962a04e1dcef3a to your computer and use it in GitHub Desktop.
Save battlecow/54676f251aabe9cfda962a04e1dcef3a to your computer and use it in GitHub Desktop.
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
}
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
}
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, &notFound) {
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