Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add workspace pkg for managing local configuration #44

Merged
merged 2 commits into from
Sep 19, 2019
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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ require (
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c
github.com/onsi/ginkgo v1.10.1
github.com/onsi/gomega v1.7.0
github.com/pkg/errors v0.8.1 // indirect
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.0
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
Expand All @@ -87,6 +88,8 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
Expand Down
24 changes: 24 additions & 0 deletions internal/pkg/archer/workspace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package archer

// WorkspaceSummary is a description of what's associated with this workspace.
type WorkspaceSummary struct {
ProjectName string `yaml:"project"`
}

// Workspace can bootstrap a workspace with a manifest directory and workspace summary
// and it can manage manifest files.
type Workspace interface {
ManifestIO
Create(projectName string) error
Summary() (*WorkspaceSummary, error)
}

// ManifestIO can read, write and list local manifest files.
type ManifestIO interface {
WriteManifest(manifestBlob []byte, applicationName string) error
ReadManifestFile(manifestFileName string) ([]byte, error)
ListManifestFiles() ([]string, error)
}
46 changes: 46 additions & 0 deletions internal/pkg/workspace/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package workspace

import "fmt"

// ErrWorkspaceNotFound means we couldn't locate a workspace root.
type ErrWorkspaceNotFound struct {
CurrentDirectory string
ManifestDirectoryName string
NumberOfLevelsChecked int
}

func (e *ErrWorkspaceNotFound) Error() string {
return fmt.Sprintf("couldn't find a directory called %s up to %d levels up from %s",
e.ManifestDirectoryName,
e.NumberOfLevelsChecked,
e.CurrentDirectory)
}

// ErrNoProjectAssociated means we couldn't locate a workspace summary file.
type ErrNoProjectAssociated struct{}

func (e *ErrNoProjectAssociated) Error() string {
return fmt.Sprint("couldn't find a project associated with this workspace")
}

// ErrWorkspaceHasExistingProject means we tried to create a workspace for a project
// but it already belongs to another project.
type ErrWorkspaceHasExistingProject struct {
ExistingProjectName string
}

func (e *ErrWorkspaceHasExistingProject) Error() string {
return fmt.Sprintf("this workspace is already registered with project %s", e.ExistingProjectName)
}

// ErrManifestNotFound means we we couldn't find a manifest in the manifest root.
type ErrManifestNotFound struct {
ManifestName string
}

func (e *ErrManifestNotFound) Error() string {
return fmt.Sprintf("manifest file %s does not exists", e.ManifestName)
}
234 changes: 234 additions & 0 deletions internal/pkg/workspace/workspace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package workspace contains the service to manage a user's local workspace. This includes
// creating a manifest directory, reading and writing a summary file
// to that directory and managing manifest files (reading, writing and listing).
// The typical workspace will be structured like:
// .
// ├── ecs (manifest directory)
// │ ├── .project (workspace summary)
// │ └── my-app.yml (manifest)
// └── my-app (customer application)
//
package workspace

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/aws/PRIVATE-amazon-ecs-archer/internal/pkg/archer"
"github.com/spf13/afero"
"gopkg.in/yaml.v3"
)

const (
workspaceSummaryFileName = ".project"
manifestDirectoryName = "ecs"
maximumParentDirsToSearch = 5
manifestFileSuffix = "-app.yml"
)

// Service manages a local workspace, including creating and managing manifest files.
type Service struct {
workingDir string
manifestDir string
fsUtils *afero.Afero
}

// New returns a workspace Service, used for reading and writing to
// user's local workspace.
func New() (*Service, error) {
appFs := afero.NewOsFs()
fsUtils := &afero.Afero{Fs: appFs}

workingDir, err := os.Getwd()
if err != nil {
return nil, err
}
ws := Service{
workingDir: workingDir,
fsUtils: fsUtils,
}

return &ws, nil
}

// Create creates the manifest directory (if it doesn't already exist) in
// the current working directory, and saves a summary in the manifest
// directory with the project name.
func (ws *Service) Create(projectName string) error {
// Create a manifest directory, if one doesn't exist
if createDirErr := ws.createManifestDirectory(); createDirErr != nil {
return createDirErr
}

// Grab an existing workspace summary, if one exists.
existingWorkspaceSummary, existingWorkspaceSummaryErr := ws.Summary()

if existingWorkspaceSummaryErr == nil {
// If a summary exists, but is registered to a different project, throw an error.
if existingWorkspaceSummary.ProjectName != projectName {
return &ErrWorkspaceHasExistingProject{ExistingProjectName: existingWorkspaceSummary.ProjectName}
}
// Otherwise our work is all done.
return nil
}

// If there isn't an existing workspace summary, create it.
var noProjectFound *ErrNoProjectAssociated
if errors.As(existingWorkspaceSummaryErr, &noProjectFound) {
return ws.writeSummary(projectName)
}

return existingWorkspaceSummaryErr
}

// Summary returns a summary of the workspace - including the project name.
func (ws *Service) Summary() (*archer.WorkspaceSummary, error) {
summaryPath, err := ws.summaryPath()
if err != nil {
return nil, err
}
summaryFileExists, err := ws.fsUtils.Exists(summaryPath)
if summaryFileExists {
value, err := ws.fsUtils.ReadFile(summaryPath)
if err != nil {
return nil, err
}
wsSummary := archer.WorkspaceSummary{}
return &wsSummary, yaml.Unmarshal(value, &wsSummary)
}
return nil, &ErrNoProjectAssociated{}
}

func (ws *Service) writeSummary(projectName string) error {
summaryPath, err := ws.summaryPath()
if err != nil {
return err
}

workspaceSummary := archer.WorkspaceSummary{
ProjectName: projectName,
}

serializedWorkspaceSummary, err := yaml.Marshal(workspaceSummary)

if err != nil {
return err
}
return ws.fsUtils.WriteFile(summaryPath, serializedWorkspaceSummary, 0644)
}

func (ws *Service) summaryPath() (string, error) {
manifestPath, err := ws.manifestDirectoryPath()
if err != nil {
return "", err
}
workspaceSummaryPath := filepath.Join(manifestPath, workspaceSummaryFileName)
return workspaceSummaryPath, nil
}

func (ws *Service) createManifestDirectory() error {
// First check to see if a manifest directory already exists
dirExists, err := ws.fsUtils.DirExists(filepath.Join(ws.workingDir, manifestDirectoryName))
if err != nil {
return err
}
// If a manifest directory doesn't exist, create it - otherwise fast succeed
if !dirExists {
return ws.fsUtils.Mkdir(manifestDirectoryName, 0755)
}
return nil
}

func (ws *Service) manifestDirectoryPath() (string, error) {
if ws.manifestDir != "" {
return ws.manifestDir, nil
}
// Are we in the manifest directory?
inEcsDir := filepath.Base(ws.workingDir) == manifestDirectoryName
if inEcsDir {
ws.manifestDir = ws.workingDir
return ws.manifestDir, nil
}

searchingDir := ws.workingDir
for try := 0; try < maximumParentDirsToSearch; try++ {
currentDirectoryPath := filepath.Join(searchingDir, manifestDirectoryName)
inCurrentDirPath, err := ws.fsUtils.DirExists(currentDirectoryPath)
if err != nil {
return "", err
}
if inCurrentDirPath {
ws.manifestDir = currentDirectoryPath
return ws.manifestDir, nil
}
searchingDir = filepath.Dir(searchingDir)
}
return "", &ErrWorkspaceNotFound{
CurrentDirectory: ws.workingDir,
ManifestDirectoryName: manifestDirectoryName,
NumberOfLevelsChecked: maximumParentDirsToSearch,
}
}

// ListManifestFiles returns a list of all the local manifests filenames.
func (ws *Service) ListManifestFiles() ([]string, error) {
manifestDir, err := ws.manifestDirectoryPath()
if err != nil {
return nil, err
}
manifestDirFiles, err := ws.fsUtils.ReadDir(manifestDir)
if err != nil {
return nil, err
}

var manifestFiles []string
for _, file := range manifestDirFiles {
if !file.IsDir() && strings.HasSuffix(file.Name(), manifestFileSuffix) {
manifestFiles = append(manifestFiles, file.Name())
}
}

return manifestFiles, nil
}

// ReadManifestFile takes in a manifest file (e.g. frontend-app.yml) and returns
// the read bytes.
func (ws *Service) ReadManifestFile(manifestFile string) ([]byte, error) {
manifestDirPath, err := ws.manifestDirectoryPath()
if err != nil {
return nil, err
}
manifestPath := filepath.Join(manifestDirPath, manifestFile)
manifestFileExists, err := ws.fsUtils.Exists(manifestPath)

if err != nil {
return nil, err
}

if !manifestFileExists {
return nil, &ErrManifestNotFound{ManifestName: manifestFile}
}

value, err := ws.fsUtils.ReadFile(manifestPath)
if err != nil {
return nil, err
}

return value, nil
}

// WriteManifest takes a manifest blob and writes it to the manifest directory.
func (ws *Service) WriteManifest(manifestBlob []byte, applicationName string) error {
manifestPath, err := ws.manifestDirectoryPath()
if err != nil {
return err
}
manifestFileName := fmt.Sprintf("%s%s", applicationName, manifestFileSuffix)
return ws.fsUtils.WriteFile(filepath.Join(manifestPath, manifestFileName), manifestBlob, 0644)
}
Loading