diff --git a/Dockerfile b/Dockerfile index ee3f7fe..52374ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,11 @@ FROM golang:1.14.0-alpine AS builder +RUN apk --no-cache add make WORKDIR $GOPATH/src/github.com/mcuadros/ascode COPY . . -RUN GO111MODULE=on CGO_ENABLED=0 GOPROXY="https://proxy.golang.org" go build -o /bin/ascode . +RUN GO111MODULE=on CGO_ENABLED=0 GOPROXY="https://proxy.golang.org" \ + go build \ + -ldflags "$(make goldflags)" \ + -o /bin/ascode . FROM alpine:latest RUN apk --no-cache add ca-certificates diff --git a/Makefile b/Makefile index 9683827..c232fca 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,7 @@ -# Package configuration -PROJECT = ascode -COMMANDS = . -DEPENDENCIES = - # Documentation OUTLINE_CMD ?= outline DOCUMENTATION_PATH ?= _documentation -DOCUMENTATION_RUNTIME_PATH ?= $(DOCUMENTATION_PATH)/runtime +DOCUMENTATION_RUNTIME_PATH ?= $(DOCUMENTATION_PATH)/reference EXAMPLES_PATH ?= starlark/types/testdata/examples RUNTIME_MODULES = \ @@ -20,32 +15,13 @@ RUNTIME_MODULES = \ github.com/qri-io/starlib/re \ github.com/qri-io/starlib/http -# Build information -BUILD ?= $(shell date +"%m-%d-%Y_%H_%M_%S") -COMMIT ?= $(shell git rev-parse --short HEAD) -GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "-dirty" || true) -DEV_PREFIX := dev -VERSION ?= $(DEV_PREFIX)-$(COMMIT)$(GIT_DIRTY) +# Build Info +GO_LDFLAGS_CMD = go run _scripts/goldflags.go +GO_LDFLAGS_PACKAGE = cmd +GO_LDFLAGS_PACKAGES = \ + starlarkVersion=go.starlark.net \ + terraformVersion=github.com/hashicorp/terraform -# Travis CI -ifneq ($(TRAVIS_TAG), ) - VERSION := $(TRAVIS_TAG) -endif - -# Packages content -PKG_OS ?= darwin linux -PKG_ARCH = amd64 - -# Golang config -LD_FLAGS ?= -X main.version=$(VERSION) -X main.build=$(BUILD) -X main.commit=$(COMMIT) -GO_CMD = go -GO_GET = $(GO_CMD) get -v -t -GO_BUILD = $(GO_CMD) build -ldflags "$(LD_FLAGS)" -GO_TEST = $(GO_CMD) test -v - -# Environment -BUILD_PATH := build -BIN_PATH := $(BUILD_PATH)/bin # Rules .PHONY: $(RUNTIME_MODULES) $(COMMANDS) documentation @@ -58,30 +34,5 @@ $(RUNTIME_MODULES): $(DOCUMENTATION_RUNTIME_PATH) $(DOCUMENTATION_RUNTIME_PATH): mkdir -p $@ -build: $(COMMANDS) -$(COMMANDS): - @if [ "$@" == "." ]; then \ - BIN=`basename $(CURDIR)` ; \ - else \ - BIN=`basename $@` ; \ - fi && \ - for os in $(PKG_OS); do \ - NBIN="$${BIN}" ; \ - if [ "$${os}" == windows ]; then \ - NBIN="$${NBIN}.exe"; \ - fi && \ - for arch in $(PKG_ARCH); do \ - mkdir -p $(BUILD_PATH)/$(PROJECT)_$${os}_$${arch} && \ - $(GO_BUILD_ENV) GOOS=$${os} GOARCH=$${arch} \ - $(GO_BUILD) -o "$(BUILD_PATH)/$(PROJECT)_$${os}_$${arch}/$${NBIN}" ./$@; \ - done; \ - done - -packages: build - @cd $(BUILD_PATH); \ - for os in $(PKG_OS); do \ - for arch in $(PKG_ARCH); do \ - TAR_VERSION=`echo $(VERSION) | tr "/" "-"`; \ - tar -cvzf $(PROJECT)_$${TAR_VERSION}_$${os}_$${arch}.tar.gz $(PROJECT)_$${os}_$${arch}/; \ - done; \ - done \ No newline at end of file +goldflags: + @$(GO_LDFLAGS_CMD) $(GO_LDFLAGS_PACKAGE) . $(GO_LDFLAGS_PACKAGES) \ No newline at end of file diff --git a/_scripts/goldflags.go b/_scripts/goldflags.go new file mode 100644 index 0000000..898b4c9 --- /dev/null +++ b/_scripts/goldflags.go @@ -0,0 +1,134 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/rogpeppe/go-internal/modfile" +) + +func main() { + pkg := os.Args[1] + path := os.Args[2] + + f, err := readGoMod(path) + if err != nil { + log.Fatal(err) + } + + flags, err := getFlags(f, path, os.Args[3:]) + if err != nil { + log.Fatal(err) + } + + if pkg != "main" { + pkg = filepath.Join(f.Module.Mod.Path, pkg) + } + + fmt.Printf(renderLDFLAGS(pkg, flags)) +} + +func getFlags(f *modfile.File, path string, pkgs []string) (map[string]string, error) { + var err error + flags := make(map[string]string, 0) + flags["build"] = time.Now().Format(time.RFC3339) + flags["version"], flags["commit"], err = readVersion(path) + + if err != nil { + return nil, err + } + + for _, v := range pkgs { + parts := strings.SplitN(v, "=", 2) + key := parts[0] + pkg := parts[1] + + flags[key] = getPackageVersion(f, pkg) + } + + return flags, nil +} + +func readVersion(path string) (string, string, error) { + r, err := git.PlainOpen(path) + if err != nil { + return "", "", err + } + + ref, err := r.Head() + if err != nil { + return "", "", err + } + + if !ref.Name().IsBranch() { + ref, err = findTag(r, ref.Hash()) + if err != nil { + return "", "", err + } + } + + return ref.Name().Short(), ref.Hash().String()[:7], nil +} + +func findTag(r *git.Repository, h plumbing.Hash) (*plumbing.Reference, error) { + tagrefs, err := r.Tags() + if err != nil { + return nil, err + } + + var match *plumbing.Reference + err = tagrefs.ForEach(func(t *plumbing.Reference) error { + if t.Hash() == h { + match = t + } + + return nil + }) + + return match, err +} + +func getVersionFromBranch(ref *plumbing.Reference) string { + name := ref.Name().Short() + pattern := "dev-%s" + if name != "master" { + pattern = fmt.Sprintf("dev-%s-%%s", name) + } + + hash := ref.Hash().String()[:7] + return fmt.Sprintf(pattern, hash) +} + +func readGoMod(path string) (*modfile.File, error) { + content, err := ioutil.ReadFile(filepath.Join(path, "go.mod")) + if err != nil { + return nil, err + } + + return modfile.ParseLax("", content, nil) +} + +func getPackageVersion(f *modfile.File, pkg string) string { + for _, r := range f.Require { + if r.Mod.Path == pkg { + return r.Mod.Version + } + } + return "" +} + +func renderLDFLAGS(pkg string, flags map[string]string) string { + output := make([]string, 0) + for k, v := range flags { + output = append(output, fmt.Sprintf("-X %s.%s=%s", pkg, k, v)) + } + + return strings.Join(output, " ") +} diff --git a/cmd/ascode-doc/doc/.providers/terraform-provider-ignition_v1.1.0_x4 b/cmd/ascode-doc/doc/.providers/terraform-provider-ignition_v1.1.0_x4 deleted file mode 100755 index 7c3283f..0000000 Binary files a/cmd/ascode-doc/doc/.providers/terraform-provider-ignition_v1.1.0_x4 and /dev/null differ diff --git a/cmd/ascode-doc/doc/fixtures/ignition_file.md b/cmd/ascode-doc/doc/fixtures/ignition_file.md deleted file mode 100644 index d35abbb..0000000 --- a/cmd/ascode-doc/doc/fixtures/ignition_file.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -layout: "ignition" -page_title: "Ignition: ignition_file" -sidebar_current: "docs-ignition-datasource-file" -description: |- - Describes a file to be written in a particular filesystem. ---- - -# ignition\_file - -Describes a file to be written in a particular filesystem. - -## Example Usage - -File with inline content: - -```hcl -data "ignition_file" "hello" { - filesystem = "foo" - path = "/hello.txt" - content { - content = "Hello World!" - } -} -``` - -File with remote content: - -```hcl -data "ignition_file" "hello" { - filesystem = "qux" - path = "/hello.txt" - source { - source = "http://example.com/hello.txt.gz" - compression = "gzip" - verification = "sha512-0123456789abcdef0123456789...456789abcdef" - } -} -``` - -## Argument Reference - -The following arguments are supported: - -* `filesystem` - (Required) The internal identifier of the filesystem. This matches the last filesystem with the given identifier. This should be a valid name from a _ignition\_filesystem_ resource. - -* `path` - (Required) The absolute path to the file. - -* `content` - (Optional) Block to provide the file content inline. - -* `source` - (Optional) Block to retrieve the file content from a remote location. - - __Note__: `content` and `source` are mutually exclusive. - -* `mode` - (Optional) The file's permission mode. The mode must be properly specified as a decimal value (i.e. 0644 -> 420). - -* `uid` - (Optional) The user ID of the owner. - -* `gid` - (Optional) The group ID of the owner. - -The `content` block supports: - -* `mime` - (Required) MIME format of the content (default _text/plain_). - -* `content` - (Required) Content of the file. - -The `source` block supports: - -* `source` - (Required) The URL of the file contents. Supported schemes are http, https, tftp, s3, and [data][rfc2397]. When using http, it is advisable to use the verification option to ensure the contents haven't been modified. - -* `compression` - (Optional) The type of compression used on the contents (null or gzip). Compression cannot be used with S3. - -* `verification` - (Optional) The hash of the config, in the form _\-\_ where type is sha512. - -## Attributes Reference - -The following attributes are exported: - -* `id` - ID used to reference this resource in _ignition_config_. - -[rfc2397]: https://tools.ietf.org/html/rfc2397 \ No newline at end of file diff --git a/cmd/ascode-doc/doc/provider.go b/cmd/ascode-doc/doc/provider.go deleted file mode 100644 index 4242851..0000000 --- a/cmd/ascode-doc/doc/provider.go +++ /dev/null @@ -1,225 +0,0 @@ -package doc - -import ( - "bufio" - "fmt" - "io" - "os" - "regexp" - "strings" - - "github.com/b5/outline/lib" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/storage/memory" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/providers" - "github.com/mcuadros/ascode/starlark/types" -) - -type ResourceDocumentation struct { - Name string - Type string - Attribs map[string]string - Blocks map[string]map[string]string -} - -func NewResourceDocumentation(typ, name string) *ResourceDocumentation { - return &ResourceDocumentation{ - Name: name, - Type: typ, - Attribs: make(map[string]string, 0), - Blocks: make(map[string]map[string]string, 0), - } -} - -var re = regexp.MustCompile(`\*[. ][\x60](.*)[\x60].*\) (.*)`) - -// https://regex101.com/r/hINfBI/2 -var blockRe = regexp.MustCompile(`^[^\*].*\x60(.*)\x60 block(?:s?)`) - -func (r *ResourceDocumentation) Decode(doc io.Reader) error { - var block string - - buf := bufio.NewReader(doc) - for { - line, err := buf.ReadString('\n') - if err == io.EOF { - return nil - } - - if err != nil { - return err - } - - line = strings.TrimSpace(line) - if len(line) == 0 { - continue - } - - parts := re.FindStringSubmatch(line) - if len(parts) == 3 { - if block == "" { - r.AddAttrib(parts[1], parts[2]) - continue - } - - r.AddBlockAttrib(block, parts[1], parts[2]) - continue - } - - parts = blockRe.FindStringSubmatch(line) - if len(parts) == 2 { - block = parts[1] - } - } - - return nil -} - -func (r *ResourceDocumentation) AddAttrib(name, desc string) { - r.Attribs[name] = desc -} - -func (r *ResourceDocumentation) AddBlockAttrib(block, name, desc string) { - if _, ok := r.Blocks[block]; !ok { - r.Blocks[block] = make(map[string]string, 0) - } - - r.Blocks[block][name] = desc -} - -type Documentation struct { - name string - repository *git.Repository - head *object.Commit - resources map[string]map[string]string -} - -func NewDocumentation(name string) (*Documentation, error) { - d := &Documentation{ - name: name, - resources: make(map[string]map[string]string, 0), - } - - return d, d.initRepository() -} - -func (d *Documentation) initRepository() error { - storer := memory.NewStorage() - - var err error - d.repository, err = git.Clone(storer, nil, &git.CloneOptions{ - URL: fmt.Sprintf("https://github.com/terraform-providers/terraform-provider-%s.git", d.name), - Depth: 1, - - // as git does, when you make a clone, pull or some other operations the - // server sends information via the sideband, this information can being - // collected provinding a io.Writer to the CloneOptions options - Progress: os.Stdout, - }) - - h, err := d.repository.Head() - if err != nil { - return err - } - - d.head, err = d.repository.CommitObject(h.Hash()) - return err -} - -func (d *Documentation) Resource(typ, name string) (*ResourceDocumentation, error) { - parts := strings.SplitN(name, "_", 2) - name = parts[1] - - filename := fmt.Sprintf("website/docs/%s/%s.html.md", typ, name) - - file, err := d.head.File(filename) - if err != nil { - return nil, err - } - - r, err := file.Reader() - if err != nil { - return nil, err - } - - resource := NewResourceDocumentation(typ, name) - return resource, resource.Decode(r) -} - -func (d *Documentation) Do(name string, schema providers.GetSchemaResponse) *lib.Doc { - doc := &lib.Doc{} - doc.Name = name - doc.Path = name - - for name, schema := range schema.DataSources { - doc.Types = append(doc.Types, d.schemaToDoc(name, &schema)...) - } - - return doc -} - -func (d *Documentation) schemaToDoc(resource string, s *providers.Schema) []*lib.Type { - rd, err := d.Resource("d", resource) - if err != nil { - panic(err) - } - - fmt.Println(resource, rd) - typ := &lib.Type{} - typ.Name = resource - - for name, attr := range s.Block.Attributes { - typ.Fields = append(typ.Fields, d.attributeToField(rd.Attribs, name, attr)) - } - - types := []*lib.Type{typ} - - for name, block := range s.Block.BlockTypes { - types = append(types, d.blockToType(rd, resource, name, block)) - typ.Fields = append(typ.Fields, d.blockToField(rd, resource, name, block)) - } - - return types -} - -func (d *Documentation) blockToType(rd *ResourceDocumentation, resource, name string, block *configschema.NestedBlock) *lib.Type { - typ := &lib.Type{} - typ.Name = fmt.Sprintf("%s.%s", resource, name) - - for n, attr := range block.Attributes { - //fmt.Println(rd.Blocks[name]) - - typ.Fields = append(typ.Fields, d.attributeToField(rd.Blocks[name], n, attr)) - } - - return typ -} - -func (d *Documentation) blockToField(rd *ResourceDocumentation, resource, name string, block *configschema.NestedBlock) *lib.Field { - field := &lib.Field{} - nested := fmt.Sprintf("%s.%s", resource, name) - - field.Name = name - if block.MaxItems != 1 { - field.Type = fmt.Sprintf("collection<%s>", nested) - } else { - field.Type = nested - } - - return field -} - -func (d *Documentation) attributeToField(doc map[string]string, name string, attr *configschema.Attribute) *lib.Field { - field := &lib.Field{} - field.Name = name - field.Description, _ = doc[name] - if attr.Computed && !attr.Optional { - field.Type = "computed" - } else { - field.Type = types.MustTypeFromCty(attr.Type).Starlark() - } - - return field -} diff --git a/cmd/ascode-doc/doc/provider_test.go b/cmd/ascode-doc/doc/provider_test.go deleted file mode 100644 index 321f35f..0000000 --- a/cmd/ascode-doc/doc/provider_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package doc - -import ( - "fmt" - "io/ioutil" - "os" - "strings" - "testing" - "text/template" - - "github.com/hashicorp/terraform/plugin" - "github.com/mcuadros/ascode/terraform" - "github.com/stretchr/testify/assert" -) - -func TestDocumentation(t *testing.T) { - f, err := os.Open("fixtures/ignition_file.md") - assert.NoError(t, err) - - res := NewResourceDocumentation("d", "ignition_file") - res.Decode(f) - - assert.Len(t, res.Attribs, 7) - assert.Len(t, res.Blocks["content"], 2) - assert.Len(t, res.Blocks["source"], 3) -} - -func TestDo(t *testing.T) { - t.Skip() - - pm := &terraform.PluginManager{".providers"} - cli, meta, err := pm.Provider("ignition", "1.1.0", false) - if err != nil { - panic(err) - } - - fmt.Println(meta) - - rpc, err := cli.Client() - if err != nil { - panic(err) - } - - raw, err := rpc.Dispense(plugin.ProviderPluginName) - if err != nil { - panic(err) - } - - provider := raw.(*plugin.GRPCProvider) - response := provider.GetSchema() - - rd, err := NewDocumentation("ignition") - assert.NoError(t, err) - - doc := rd.Do(meta.Name, response) - - str, err := ioutil.ReadFile("../../_scripts/template.md") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - tplFuncMap := make(template.FuncMap) - tplFuncMap["split"] = strings.Split - - temp, err := template.New("foo").Funcs(tplFuncMap).Parse(string(str)) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if err := temp.Execute(os.Stdout, []interface{}{doc}); err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } -} diff --git a/cmd/repl.go b/cmd/repl.go index 955b2b0..0364408 100644 --- a/cmd/repl.go +++ b/cmd/repl.go @@ -2,7 +2,7 @@ package cmd const ( REPLCmdShortDescription = "Run as interactive shell." - REPLCmdLongDescription = RunCmdShortDescription + "\n\n" + + REPLCmdLongDescription = REPLCmdShortDescription + "\n\n" + "The REPL shell provides the same capabilities as the regular `run`\n" + "command." ) diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..06fcf90 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "fmt" + "runtime" +) + +const ( + VersionCmdShortDescription = "Version prints information about this binary." + VersionCmdLongDescription = VersionCmdShortDescription + "\n\n" + + "Includes build information about AsCode like version and build\n" + + "date but also versions from the Go runtime and other dependencies." +) + +var version string +var commit string +var build string +var terraformVersion string +var starlarkVersion string + +type VersionCmd struct{} + +func (c *VersionCmd) Execute(args []string) error { + fmt.Printf("Go Version: %s\n", runtime.Version()) + fmt.Printf("AsCode Version: %s\n", version) + fmt.Printf("AsCode Commit: %s\n", commit) + fmt.Printf("AsCode Build Date: %s\n", build) + fmt.Printf("Terraform Version: %s\n", terraformVersion) + fmt.Printf("Starlark Version: %s\n", starlarkVersion) + + return nil +} diff --git a/main.go b/main.go index 1915d8d..ceebfed 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "os" "github.com/jessevdk/go-flags" @@ -13,13 +12,13 @@ var build string func main() { parser := flags.NewNamedParser("ascode", flags.Default) - parser.LongDescription = "AsCode - The real infrastructure as code." + parser.LongDescription = "AsCode - Terraform Alternative Syntax." parser.AddCommand("run", cmd.RunCmdShortDescription, cmd.RunCmdLongDescription, &cmd.RunCmd{}) parser.AddCommand("repl", cmd.REPLCmdShortDescription, cmd.REPLCmdLongDescription, &cmd.REPLCmd{}) + parser.AddCommand("version", cmd.VersionCmdShortDescription, cmd.VersionCmdLongDescription, &cmd.VersionCmd{}) if _, err := parser.Parse(); err != nil { if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { - fmt.Printf("Build information\n commit: %s\n date:%s\n", version, build) os.Exit(0) }