Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# SSM and S3 Server
# AWS Server

This project provides an HTTP server using Go, which interacts with AWS Systems Manager (SSM) to fetch/put parameters and with S3 to fetch/put files. This project is useful for running a sidecar, to use original images without a hassle.
The server exposes two main endpoints: `/ssm` and `/s3`.
This project provides an HTTP server using Go, which interacts with AWS Services. This project is useful for running a sidecar, to use original images without a hassle.

## Features

- Fetch decrypted parameters from AWS SSM.
- Fetch and serve files from AWS S3.
- Upload files to AWS S3.
- Fetch ECR authorization token.
- Fetch caller identity from AWS STS.
- Basic CI/CD pipeline using GitHub Actions for automatic builds and tests.

## Requirements
Expand Down Expand Up @@ -115,6 +116,30 @@ Upload a file to an S3 bucket.
curl -X POST -F 'file=@/path/to/your/file' "http://localhost:3000/s3?bucket=example-bucket&key=example-key"
```

### Get ECR Login

Fetch an authorization token for ECR.

- **URL:** `/ecr/login`
- **Method:** `GET`
- **Example:**

```sh
curl "http://localhost:3000/ecr/login" | docker login --username AWS --password-stdin aws-account-id.dkr.ecr.eu-central-1.amazonaws.com
```

### Get Caller Identity

Fetch the caller identity from AWS STS.

- **URL:** `/sts`
- **Method:** `GET`
- **Example:**

```sh
curl "http://localhost:3000/sts"
```

## Running Tests

To run the tests:
Expand Down
238 changes: 38 additions & 200 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -1,207 +1,45 @@
package main

import (
"context"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"log"
"net/http"
"os"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/leneffets/ssmserver/pkg/ecr"
"github.com/leneffets/ssmserver/pkg/s3"
"github.com/leneffets/ssmserver/pkg/ssm"
"github.com/leneffets/ssmserver/pkg/sts"
)

// Funktion, um SSM-Parameter abzurufen
func GetParameter(ctx context.Context, svc ssmiface.SSMAPI, name *string) (*ssm.GetParameterOutput, error) {
results, err := svc.GetParameterWithContext(ctx, &ssm.GetParameterInput{
Name: name,
WithDecryption: aws.Bool(true),
})
return results, err
}

// Funktion, um ein Objekt aus S3 zu holen
func GetFromS3(ctx context.Context, sess *session.Session, bucket, key string) (io.ReadCloser, error) {
svc := s3.New(sess)
output, err := svc.GetObjectWithContext(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
if err != nil {
return nil, err
}
return output.Body, nil
}

// Funktion, um ein Objekt in S3 zu legen
func PutToS3(ctx context.Context, sess *session.Session, bucket, key string, body io.ReadSeeker) error {
svc := s3.New(sess)
_, err := svc.PutObjectWithContext(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: body,
})
return err
}

// Funktion, um einen Parameter in den Parameter Store zu legen
func PutParameter(ctx context.Context, svc ssmiface.SSMAPI, name *string, value *string, typeStr *string) (*ssm.PutParameterOutput, error) {
results, err := svc.PutParameterWithContext(ctx, &ssm.PutParameterInput{
Name: name,
Value: value,
Type: aws.String(*typeStr),
})
return results, err
}

func main() {
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))

svc := ssm.New(sess)

creds, err := sess.Config.Credentials.Get()
if err != nil {
log.Fatalf("Failed to get credentials: %v", err)
}
log.Printf("Using credentials: %s/%s\n", creds.AccessKeyID, creds.SecretAccessKey)

http.HandleFunc("/ssm", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
// Validate input parameter
id := r.URL.Query().Get("name")
if id == "" {
http.Error(w, "Parameter 'name' is required", http.StatusBadRequest)
return
}

// Set context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Fetch parameter
results, err := GetParameter(ctx, svc, &id)
if err != nil {
log.Printf("Error fetching parameter: %v", err)
http.Error(w, "Error fetching parameter", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(*results.Parameter.Value))
} else if r.Method == http.MethodPost {
// Parse the form data
if err := r.ParseForm(); err != nil {
http.Error(w, "Invalid form data", http.StatusBadRequest)
log.Printf("Invalid form data: %v", err)
return
}

// Validate input parameters
name := r.FormValue("name")
value := r.FormValue("value")
typeStr := r.FormValue("type")

if name == "" || value == "" || typeStr == "" {
http.Error(w, "Parameters 'name', 'value', and 'type' are required", http.StatusBadRequest)
return
}

// Set context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Put parameter
_, err := PutParameter(ctx, svc, &name, &value, &typeStr)
if err != nil {
http.Error(w, "Error putting parameter", http.StatusInternalServerError)
log.Printf("Error putting parameter: %v", err)
return
}

log.Printf("Parameter %s uploaded successfully", name)
w.WriteHeader(http.StatusOK)
} else {
http.Error(w, "Invalid method", http.StatusMethodNotAllowed)
}
})

http.HandleFunc("/s3", func(w http.ResponseWriter, r *http.Request) {
bucket := r.URL.Query().Get("bucket")
key := r.URL.Query().Get("key")
if bucket == "" || key == "" {
http.Error(w, "Parameters 'bucket' and 'key' are required", http.StatusBadRequest)
return
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if r.Method == http.MethodGet {
body, err := GetFromS3(ctx, sess, bucket, key)
if err != nil {
http.Error(w, "Error fetching file from S3", http.StatusInternalServerError)
log.Printf("Error fetching file from S3: %v", err)
return
}
defer body.Close()

w.Header().Set("Content-Type", "application/octet-stream")
if _, err := io.Copy(w, body); err != nil {
http.Error(w, "Error sending file", http.StatusInternalServerError)
log.Printf("Error sending file: %v", err)
return
}
} else if r.Method == http.MethodPost {
file, _, err := r.FormFile("file")
if err != nil {
http.Error(w, "Error reading uploaded file", http.StatusBadRequest)
log.Printf("Error reading uploaded file: %v", err)
return
}
defer file.Close()

tempFile, err := ioutil.TempFile("", "upload-*.tmp")
if err != nil {
http.Error(w, "Error creating temporary file", http.StatusInternalServerError)
log.Printf("Error creating temporary file: %v", err)
return
}
defer os.Remove(tempFile.Name())

if _, err := io.Copy(tempFile, file); err != nil {
http.Error(w, "Error saving file", http.StatusInternalServerError)
log.Printf("Error saving file: %v", err)
return
}

tempFile.Seek(0, 0)

if err := PutToS3(ctx, sess, bucket, key, tempFile); err != nil {
http.Error(w, "Error uploading file to S3", http.StatusInternalServerError)
log.Printf("Error uploading file to S3: %v", err)
return
}

log.Printf("File uploaded successfully to bucket %s with key %s", bucket, key)
w.WriteHeader(http.StatusOK)
} else {
http.Error(w, "Invalid method", http.StatusMethodNotAllowed)
}
})

port := os.Getenv("PORT")
if port == "" {
port = "3000"
}

log.Printf("Server running on port %s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("Error starting server: %v", err)
}
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))

http.HandleFunc("/ssm", func(w http.ResponseWriter, r *http.Request) {
ssm.HandleSSM(w, r, sess)
})

http.HandleFunc("/s3", func(w http.ResponseWriter, r *http.Request) {
s3.HandleS3(w, r, sess)
})

http.HandleFunc("/ecr/login", func(w http.ResponseWriter, r *http.Request) {
ecr.HandleECRLogin(w, r, sess)
})

http.HandleFunc("/sts", func(w http.ResponseWriter, r *http.Request) {
sts.HandleSTS(w, r, sess)
})

port := os.Getenv("PORT")
if port == "" {
port = "3000"
}

log.Printf("Server running on port %s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("Error starting server: %v", err)
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module github.com/devstek/ssmserver
module github.com/leneffets/ssmserver

go 1.22.7

require github.com/aws/aws-sdk-go v1.55.5

require github.com/jmespath/go-jmespath v0.4.0 // indirect
require github.com/jmespath/go-jmespath v0.4.0 // indirect
2 changes: 1 addition & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Loading
Loading