diff --git a/.circle-ruby b/.circle-ruby deleted file mode 100644 index 434e0c0..0000000 --- a/.circle-ruby +++ /dev/null @@ -1,4 +0,0 @@ -2.6.1 -2.5.3 -2.4.5 -2.3.8 diff --git a/.gitignore b/.gitignore index 68ca1c3..b07d682 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -pkg/*.gem -coverage/ -.coveralls.yml -.bundle -Gemfile.lock +/.gopath +/bin +/vendor/ +/pkg/ +/.github +/payload.zip +/.tools/ +/Dockerfile +prospectus diff --git a/v2/.gitmodules b/.gitmodules similarity index 100% rename from v2/.gitmodules rename to .gitmodules diff --git a/v2/.pkgforge b/.pkgforge similarity index 75% rename from v2/.pkgforge rename to .pkgforge index be7e159..5a95133 100644 --- a/v2/.pkgforge +++ b/.pkgforge @@ -15,11 +15,11 @@ package( type: 'file', artifacts: [ { - source: "bin/#{@forge.name}-ng_darwin", + source: "bin/#{@forge.name}_darwin", name: "#{@forge.name}_darwin" }, { - source: "bin/#{@forge.name}-ng_linux", + source: "bin/#{@forge.name}_linux", name: "#{@forge.name}_linux" } ] diff --git a/.prospectus b/.prospectus index 96878d7..18abb0c 100644 --- a/.prospectus +++ b/.prospectus @@ -1,11 +1,13 @@ -my_slug = 'akerl/prospectus' +#!/usr/bin/env ruby + +require 'prospectus' Prospectus.extra_dep('file', 'prospectus_travis') -Prospectus.extra_dep('file', 'prospectus_gems') +Prospectus.extra_dep('file', 'prospectus_golang') item do noop - extend ProspectusGems::Gemspec.new - extend ProspectusTravis::Build.new(my_slug) + extend ProspectusGolang::Deps.new + extend ProspectusTravis::Build.new('akerl/prospectus') end diff --git a/.rspec b/.rspec deleted file mode 100644 index b7afd2d..0000000 --- a/.rspec +++ /dev/null @@ -1,2 +0,0 @@ ---format Fuubar ---color diff --git a/.rubocop.yml b/.rubocop.yml deleted file mode 100644 index a3070cd..0000000 --- a/.rubocop.yml +++ /dev/null @@ -1,5 +0,0 @@ -inherit_gem: - goodcop: .rubocop.yml -AllCops: - Include: - - 'bin/*' diff --git a/.travis.yml b/.travis.yml index 09bf858..ea9254d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,17 @@ +sudo: required dist: xenial -install: -- for i in $(cat .circle-ruby) ; do rvm install $i || exit 1 ; done -- for i in $(cat .circle-ruby) ; do rvm-exec $i bundle install || exit 1 ; done -script: -- for i in $(cat .circle-ruby) ; do rvm-exec $i bundle exec rake || exit 1 ; done +services: +- docker +env: + global: + - PKGFORGE_STATEFILE=/tmp/pkgforge +script: make +deploy: + provider: script + script: make release || travis_terminate 1 + skip_cleanup: true + on: + tags: true notifications: email: false slack: diff --git a/Gemfile b/Gemfile deleted file mode 100644 index fa75df1..0000000 --- a/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source 'https://rubygems.org' - -gemspec diff --git a/LICENSE b/LICENSE index 98ee624..74b636a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Les Aker +Copyright (c) 2019 Les Aker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/v2/Makefile b/Makefile similarity index 100% rename from v2/Makefile rename to Makefile diff --git a/v2/Makefile.local b/Makefile.local similarity index 100% rename from v2/Makefile.local rename to Makefile.local diff --git a/README.md b/README.md index ee06595..7e5c1ed 100644 --- a/README.md +++ b/README.md @@ -1,286 +1,43 @@ prospectus ========= -[![Gem Version](https://img.shields.io/gem/v/prospectus.svg)](https://rubygems.org/gems/prospectus) [![Build Status](https://img.shields.io/travis/com/akerl/prospectus.svg)](https://travis-ci.com/akerl/prospectus) -[![Coverage Status](https://img.shields.io/codecov/c/github/akerl/prospectus.svg)](https://codecov.io/github/akerl/prospectus) -[![Code Quality](https://img.shields.io/codacy/36b84b3bc7b24cd4991c4753f7788850.svg)](https://www.codacy.com/app/akerl/prospectus) +[![GitHub release](https://img.shields.io/github/release/akerl/prospectus.svg)](https://github.com/akerl/prospectus/releases) [![MIT Licensed](https://img.shields.io/badge/license-MIT-green.svg)](https://tldrlegal.com/license/mit-license) -Write short scripts in a simple DSL and use the prospectus tool to check for changes in expected vs. actual state. - -I use this for checking my [homebrew tap](https://github.com/halyard/homebrew-formulae) and [ArchLinux packages](https://github.com/amylum) for outdated package versions: it compares the version I'm packaging now against the upstream latest version. +Tool to check for changes in expected vs. actual state ## Usage -This gem reads a "./.prospectus" file to determine expected/actual state. A prospectus file can be pretty lightweight: - -``` -item do - name 'zlib' - - expected do - github_release - repo 'madler/zlib' - regex /^v(.*)$/ - end - - actual do - git_tag - regex /^(.*)-\d+$/ - end -end -``` - -Prospectus works by letting you define "items", each of which have an "expected" and "actual" block. You can specify a "name", as above, otherwise it will infer the name from the directory containing the prospectus file. - -The expected/actual blocks first define the module to be used, and then define any configuration for that module. - -To run the check, just run `prospectus` in the directory with the .prospectus file, or use `prospectus -d /path/to/directory`. - -### Parsing output - -If you're looking to parse the output with something else, consider using -j to get JSON output. - -## Included Modules - -The following modules are included with Prospectus. - -Most of the examples below will use either an expected or actual block, based on the most common use case, but any module can be used for either state. - -### git_tag - -This checks the git tag of the local repo. Supports the Regex helper - -``` -# This would use the current git tag directly -actual do - git_tag -end -``` - -``` -# This would convert v1.0.0 into 1.0.0 -actual do - git_tag - regex /^v([\d.]+)$/ -end -``` - -``` -# And this would convert v1_0_0 into 1.0.0 -actual do - git_tag - regex /^v(\d+)_(\d+)_(\d+)$/, '\1.\2.\3' -end -``` - -### git_hash - -Checks the git hash of a local repository. Supports the chdir helper. - -Will return the short hash unless the `long` argument is provided. - -Primarily used for checking git submodules. - -``` -# Returns the short hash -actual do - git_hash - dir 'submodules/my-important-other-repo' -end - -# Returns the full hash -actual do - git_hash - long - dir 'submodules/other-repo' -end -``` - -### github_release - -This checks the latest GitHub release for a repo (must be a real Release, not just a tag. Use github_tag if there isn't a Release). Supports the Regex and Filter helpers and uses the GitHub API helper for API access. To track `prerelease` Releases, use `allow_prerelease` - -``` -expected do - github_release - repo 'amylum/s6' -end -``` - -### github_tag - -This checks the latest GitHub tag for a repo. Supports the Regex and Filter helpers and uses the GitHub API helper for API access. - -``` -expected do - github_tag - repo 'reubenhwk/radvd' -end -``` - -### github_hash - -This checks the latest commit hash on GitHub. Uses the github_api helper, which requires octoauth. Designed to be used alongside the git_hash module for comparing local submodules with upstream commits. +### Check specification -Will give the 7 character short hash unless "long" is specified. +Checks must implement responses for the following commands: -``` -expected do - github_hash - repo 'akerl/keys' -end +### load -expected do - github_hash - repo 'akerl/keys' - long -end -``` +The `load` command accepts a hash with a single key, the directory being checked, and returns an array of checks with optional metadata. -### homebrew_formula +Input: `{"dir": "/path/to/main/dir"}` +Output: `[{"name": "check_N", "metadata": {"foo": "bar"}}, ...]` -Checks a Formula file for the current version. This uses the grep module, and expects the formula to be in ./Formula/$NAME.rb. +### execute -``` -actual do - homebrew_formula - name 'openssh' -end -``` +The `execute` command accepts a hash representing the check object. Metadata provided during the `load` call is included. The return value must be a Result object for the given check. -### homebrew_cask +Input: `{"dir": "/path/to/main/dir", "file": "/path/to/main/dir/.prospectus.d/checkfile", "name": "check_N", "metadata": {"foo": "bar"}}` +Output: `{"actual": "unhappy", "expected": {"type": "string", "data": {"expected": "happy"}}}` -Checks a Cask file for the current version. This uses the grep module, and expects the cask to be in ./Casks/$NAME.rb. +### fix -``` -actual do - homebrew_cask - name 'alfred' -end -``` +The `fix` command can attempt to fix a failed check automatically. It accepts a hash representing the failed result, which includes the originating check. The return value must be a Result object for the given check. -### gitlab_tag +**Note:** The check must respond to the `fix` command, but if it does not support automatic fixes, it can respond by emiting the same result object it was given. -Checks a repo on GitLab.com for its latest tag. Supports the regex helper. - -``` -actual do - gitlab_tag - repo 'procps-ng/procps' -end -``` - -### grep - -This checks a local file's contents. Supports the Regex helper, and uses the provided regex pattern to match which line of the file to use. If no regex is specified, it will use the full first line of the file. - -``` -# Searches file for OPENSSL_VERSION = 1.0.1e and returns 1.0.1e -actual do - grep - file 'Makefile' - regex /^OPENSSL_VERSION = ([\w.-]+)$/ -end -``` - -### url_xpath - -Used to parse an xpath inside a web page. Requires the nokogiri gem, and supports the Regex helper. - -The easiest way to get an xpath is usually to use Chrome's Inspector to find the element you want and right click it -> Copy -> As XPath. There are some quirks, notably that nokogiri doesn't parse the tbody tag (just remove it from the xpath that Chrome provides). - -``` -# Parses the latest tag for procps-ng -expected do - url_xpath - url 'https://gitlab.com/procps-ng/procps/tags' - xpath '/html/body/div[1]/div[2]/div[2]/div/div/div[2]/ul/li[1]/div[1]/a/strong/text()' - regex /v([\d.]+)$/ -end -``` - -### static - -Basic module for staticly defining a value. Useful for testing, and also for comparisons against a known state. - -``` -item do - expected do - static_test - set '0.0.3' - end - actual do - static_test - set '0.0.1' - end -end -``` - -## Included Helpers - -### regex - -Allows modification of result using regex. Supported by most modules, per the above modules list. - -The first argument should be a regex pattern to match against the value. Note that an error will be raised if the value does not match the provided regex. An optional second value specifies the replacement string to use; the default is '\1', which will use the first capture group from your regex. - -``` -# This would convert v1.0.0 into 1.0.0 -actual do - git_tag - regex /^v([\d.]+)$/ -end -``` - -``` -# And this would convert v1_0_0 into 1.0.0 -actual do - git_tag - regex /^v(\d+)_(\d+)_(\d+)$/, '\1.\2.\3' -end -``` - -### filter - -Allows filtering of available items. Useful for modules that must parse a list where you only care about some of the entries (like upstream repos that tag multiple packages in the same repo). - -The provided argument is the regex pattern to filter the list with. - -``` -# This filters out github tags that don't match the format 1.2.3 -expected do - github_tag - repo 'foo/bar' - filter /^[\d.]+$/ -end -``` - -### chdir - -Used to chdir to a different directory before loading the state. - -``` -actual do - git_hash - dir 'submodules/important_repo' -end -``` - -### github_api - -Used by modules to provide authenticated access to the GitHub API. Uses the [octoauth gem](https://github.com/akerl/octoauth) - -### gitlab_api - -Used by modules to provide access to GitLab's API, using [the gitlab gem](https://github.com/NARKOZ/gitlab) +Input: `{"actual": "unhappy", "expected": {"type": "string", "data": {"expected": "happy"}}, "check": {"dir": "/path/to/main/dir", "file": "/path/to/main/dir/.prospectus.d/checkfile", "name": "check_N", "metadata": {"foo": "bar"}}}` +Output: `{"actual": "happy", "expected": {"type": "string", "data": {"expected": "happy"}}}` ## Installation - gem install prospectus - ## License prospectus is released under the MIT License. See the bundled LICENSE file for details. - diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 808f96e..0000000 --- a/Rakefile +++ /dev/null @@ -1,13 +0,0 @@ -require 'bundler/gem_tasks' -require 'rspec/core/rake_task' -require 'rubocop/rake_task' - -desc 'Run tests' -RSpec::Core::RakeTask.new(:spec) - -desc 'Run Rubocop on the gem' -RuboCop::RakeTask.new(:rubocop) do |task| - task.fail_on_error = true -end - -task default: %i[spec rubocop build install] diff --git a/bin/prospectus b/bin/prospectus deleted file mode 100755 index d69037c..0000000 --- a/bin/prospectus +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env ruby - -require 'prospectus' -require 'mercenary' - -Mercenary.program(:prospectus) do |p| - p.version Prospectus::VERSION - p.description 'Tool and DSL for checking expected vs actual state' - p.syntax 'prospectus [options]' - - # rubocop:disable Metrics/LineLength - p.option :directory, '-d DIR', '--directory DIR', 'Change to directory before loading' - p.option :good_only, '-g', '--good', 'Show only items with good state' - p.option :all, '-a', '--all', 'Show all items' - p.option :quiet, '-q', '--quiet', 'Hide all non-error output' - p.option :json, '-j', '--json', 'Output results as json' - # rubocop:enable Metrics/LineLength - - p.action do |_, options| - options[:directory] ||= '.' - Dir.chdir(options[:directory]) do - results = Prospectus.load(options) - unless options[:quiet] - if options[:json] - puts results.to_json - else - results.each { |x| puts "#{x.name}: #{x.actual} / #{x.expected}" } - end - end - exit 1 unless results.empty? || options[:all] || options[:good_only] - end - end -end diff --git a/v2/cmd/check.go b/cmd/check.go similarity index 65% rename from v2/cmd/check.go rename to cmd/check.go index e27b775..7810285 100644 --- a/v2/cmd/check.go +++ b/cmd/check.go @@ -1,9 +1,10 @@ package cmd import ( + "encoding/json" "fmt" - "github.com/akerl/prospectus/checks" + "github.com/akerl/prospectus/v2/plugin" "github.com/spf13/cobra" ) @@ -37,27 +38,38 @@ func checkRunner(cmd *cobra.Command, args []string) error { params = args } - c, err := checks.NewSet(params) + as, err := plugin.NewSet(params) if err != nil { return err } - results := c.Execute() + results := as.Check() if err != nil { return err } if !flagAll { - results = results.Changed() + results = changedResults(results) } var output string if flagJSON { - output, err = results.JSON() + outputBytes, err := json.MarshalIndent(results, "", " ") if err != nil { return err } + output = string(outputBytes) } else { output = results.String() } fmt.Println(output) return nil } + +func changedResults(rs plugin.ResultSet) plugin.ResultSet { + newResults := plugin.ResultSet{} + for _, item := range rs { + if !item.Matches { + newResults = append(newResults, item) + } + } + return newResults +} diff --git a/cmd/fix.go b/cmd/fix.go new file mode 100644 index 0000000..93901ab --- /dev/null +++ b/cmd/fix.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/akerl/prospectus/v2/plugin" + + "github.com/spf13/cobra" +) + +var fixCmd = &cobra.Command{ + Use: "fix", + Short: "Attempt to fix items where expected state differs from actual state", + RunE: fixRunner, +} + +func init() { + rootCmd.AddCommand(fixCmd) + f := fixCmd.Flags() + f.Bool("json", false, "Print output as JSON") +} + +func fixRunner(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + flagJSON, err := flags.GetBool("json") + if err != nil { + return err + } + + params := []string{"."} + if len(args) != 0 { + params = args + } + + as, err := plugin.NewSet(params) + if err != nil { + return err + } + results := as.Check().Fix() + + var output string + if flagJSON { + outputBytes, err := json.MarshalIndent(results, "", " ") + if err != nil { + return err + } + output = string(outputBytes) + } else { + output = results.String() + } + fmt.Println(output) + return nil +} diff --git a/v2/cmd/list.go b/cmd/list.go similarity index 62% rename from v2/cmd/list.go rename to cmd/list.go index a8c3f65..6d5bf30 100644 --- a/v2/cmd/list.go +++ b/cmd/list.go @@ -3,9 +3,8 @@ package cmd import ( "encoding/json" "fmt" - "strings" - "github.com/akerl/prospectus/checks" + "github.com/akerl/prospectus/v2/plugin" "github.com/spf13/cobra" ) @@ -34,27 +33,21 @@ func listRunner(cmd *cobra.Command, args []string) error { params = args } - cs, err := checks.NewSet(params) + as, err := plugin.NewSet(params) if err != nil { return err } - if cs == nil { - cs = checks.CheckSet{} - } - var output strings.Builder + var output string if flagJSON { - outputBytes, err := json.MarshalIndent(cs, "", " ") + outputBytes, err := json.MarshalIndent(as, "", " ") if err != nil { return err } - output.Write(outputBytes) + output = string(outputBytes) } else { - for _, item := range cs { - output.WriteString(item.String()) - output.WriteString("\n") - } + output = as.String() } - fmt.Println(output.String()) + fmt.Println(output) return nil } diff --git a/v2/cmd/root.go b/cmd/root.go similarity index 100% rename from v2/cmd/root.go rename to cmd/root.go diff --git a/v2/cmd/version.go b/cmd/version.go similarity index 100% rename from v2/cmd/version.go rename to cmd/version.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5b5f942 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/akerl/prospectus/v2 + +go 1.12 + +require ( + github.com/akerl/timber/v2 v2.0.1 + github.com/ghodss/yaml v1.0.0 + github.com/spf13/cobra v0.0.5 +) diff --git a/v2/go.sum b/go.sum similarity index 83% rename from v2/go.sum rename to go.sum index baada52..c15b8f2 100644 --- a/v2/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/akerl/timber/v2 v2.0.1 h1:hY4VCOJns7KsxwxP/ifSt3Rz9GZCfKewapaimObnA2E= +github.com/akerl/timber/v2 v2.0.1/go.mod h1:jBjRGI2CWuvbZlrZkp1JO/X51pMlbg72NFy+Vnd59oI= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -6,7 +8,10 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -28,5 +33,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/prospectus.rb b/lib/prospectus.rb deleted file mode 100644 index a3f828b..0000000 --- a/lib/prospectus.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'logcabin' - -## -# Tool and DSL for checking expected vs actual state -module Prospectus - class << self - ## - # Insert a helper .new() method for creating a new Cache object - def new(*args) - self::List.new(*args) - end - - def load(*args) - self::Loader.new(*args).load - end - - ## - # Method for loading list from DSL - def load_from_file(params = {}) - file = params[:file] || raise('File path required for load_from_file') - list = List.new(params) - dsl = ListDSL.new(list, params) - dsl.instance_eval(File.read(file), File.realpath(file, Dir.pwd)) - list - end - - def modules - @modules ||= LogCabin.new(load_path: load_path(:modules)) - end - - def helpers - @helpers ||= LogCabin.new(load_path: load_path(:helpers)) - end - - def extra_dep(name, dep) - require dep - rescue LoadError - raise("The #{name} module requires the #{dep} gem") - end - - private - - def gem_dir - Gem::Specification.find_by_name('prospectus').gem_dir - end - - def load_path(type) - File.join(gem_dir, 'lib', 'prospectus', type.to_s) - end - end -end - -require 'prospectus/version' -require 'prospectus/loader' -require 'prospectus/list' -require 'prospectus/item' -require 'prospectus/state' diff --git a/lib/prospectus/helpers/chdir.rb b/lib/prospectus/helpers/chdir.rb deleted file mode 100644 index 2c816cd..0000000 --- a/lib/prospectus/helpers/chdir.rb +++ /dev/null @@ -1,18 +0,0 @@ -module LogCabin - module Modules - ## - # Change directory before running module - module Chdir - def chdir_helper - @dir ||= '.' - Dir.chdir(@dir) { yield } - end - - private - - def dir(value) - @dir = value - end - end - end -end diff --git a/lib/prospectus/helpers/filter.rb b/lib/prospectus/helpers/filter.rb deleted file mode 100644 index 9c40642..0000000 --- a/lib/prospectus/helpers/filter.rb +++ /dev/null @@ -1,20 +0,0 @@ -module LogCabin - module Modules - ## - # Use regex to filter out from a list of matches - module Filter - def filter_helper(list) - return list unless @filter_regex - new_list = list.select { |x| x =~ @filter_regex } - return new_list unless new_list.empty? - raise("No matches found in list: #{@filter_regex} / #{list}") - end - - private - - def filter(regex) - @filter_regex = regex - end - end - end -end diff --git a/lib/prospectus/helpers/github_api.rb b/lib/prospectus/helpers/github_api.rb deleted file mode 100644 index 7e6f935..0000000 --- a/lib/prospectus/helpers/github_api.rb +++ /dev/null @@ -1,52 +0,0 @@ -Prospectus.extra_dep('github_api', 'octoauth') - -module LogCabin - module Modules - ## - # Provide an api method for modules to query GitHub - module GithubApi - def github_api - cached_clients[@endpoint] ||= Octokit::Client.new(octokit_args) - end - - private - - def octokit_args - args = { - access_token: auth.token, - auto_paginate: true - } - args[:api_endpoint] = @endpoint if @endpoint - args - end - - private - - def cached_clients - @cached_clients ||= {} - end - - def auth - @auth ||= Octoauth.new(octoauth_args) - end - - def octoauth_args - args = { - note: 'Prospectus', - file: :default, - autosave: true - } - args[:api_endpoint] = @endpoint if @endpoint - args - end - - def repo(value) - @repo = value - end - - def endpoint(value) - @endpoint = value - end - end - end -end diff --git a/lib/prospectus/helpers/gitlab_api.rb b/lib/prospectus/helpers/gitlab_api.rb deleted file mode 100644 index 3452869..0000000 --- a/lib/prospectus/helpers/gitlab_api.rb +++ /dev/null @@ -1,44 +0,0 @@ -Prospectus.extra_dep('gitlab_api', 'keylime') -Prospectus.extra_dep('gitlab_api', 'gitlab') - -module LogCabin - module Modules - ## - # Provide an api method for modules to query GitLab - module GitlabApi - def gitlab_api - @gitlab_api ||= Gitlab.client( - endpoint: gitlab_endpoint + '/api/v4', - private_token: gitlab_token - ) - end - - private - - def gitlab_token - @gitlab_token ||= token_from_file - @gitlab_token ||= Keylime.new( - server: gitlab_endpoint, - account: 'prospectus' - ).get!("GitLab API token (#{gitlab_endpoint}/profile/account)").password - end - - def token_from_file - return unless File.exist? File.expand_path('~/.gitlab_api') - File.read('~/.gitlab_api').strip - end - - def gitlab_endpoint - @gitlab_endpoint ||= 'https://gitlab.com' - end - - def repo(value) - @repo = value - end - - def endpoint(value) - @gitlab_endpoint = value - end - end - end -end diff --git a/lib/prospectus/helpers/regex.rb b/lib/prospectus/helpers/regex.rb deleted file mode 100644 index 06918fd..0000000 --- a/lib/prospectus/helpers/regex.rb +++ /dev/null @@ -1,21 +0,0 @@ -module LogCabin - module Modules - ## - # Use regex to adjust state value - module Regex - def regex_helper(value) - return value unless @find - m = value.match(@find) - raise("Value does not match regex: #{value}") unless m - m.to_s.sub(@find, @replace) - end - - private - - def regex(find, replace = '\1') - @find = find - @replace = replace - end - end - end -end diff --git a/lib/prospectus/item.rb b/lib/prospectus/item.rb deleted file mode 100644 index ba13eb8..0000000 --- a/lib/prospectus/item.rb +++ /dev/null @@ -1,85 +0,0 @@ -require 'json' - -module Prospectus - ## - # Define item objects that defined expected vs actual state - class Item - attr_reader :list - - def initialize(params = {}) - @options = params - @list = List.new(params) - @dir = Dir.pwd - end - - def name - return @name if @name - @name = File.basename @dir - @name << "::#{File.basename @options[:file]}" if @options[:suffix_file] - @name - end - - def prefix(value) - raise('Name not set for sub-item') unless @name - @name = value + '::' + @name - end - - def noop - x = State.new - x.value = 'noop' - @expected = x - @actual = x - end - - def expected - @expected || raise("No expected state was loaded for #{name}") - end - - def actual - @actual || raise("No actual state was loaded for #{name}") - end - - def to_json(_ = {}) - { name: name, expected: expected.value, actual: actual.value }.to_json - end - end - - ## - # DSL for wrapping eval of item files - class ItemDSL - def initialize(item, params) - @item = item - @options = params - end - - def name(value) - @item.instance_variable_set(:@name, value) - end - - def noop - @item.noop - end - - def expected(&block) - state(:@expected, &block) - end - - def actual(&block) - state(:@actual, &block) - end - - def deps(&block) - dsl = ListDSL.new(@item.list, @options) - dsl.instance_eval(&block) - end - - private - - def state(name, &block) - state = Prospectus::State.from_block(@options, &block) - @item.instance_variable_set(name, state) - rescue => e # rubocop:disable Style/RescueStandardError - raise("Failed to set #{name} state for #{@item.name}: #{e.message}") - end - end -end diff --git a/lib/prospectus/list.rb b/lib/prospectus/list.rb deleted file mode 100644 index 1bfd597..0000000 --- a/lib/prospectus/list.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Prospectus - ## - # Define list object that contains items - class List - def initialize(params = {}) - @options = params - end - - def items - @items ||= [] - end - - def check - all, good_only = @options.values_at(:all, :good_only) - items.select do |x| - match = x.actual =~ x.expected - true if all || (!match ^ good_only) - end - end - end - - ## - # DSL for wrapping eval of list files - class ListDSL - def initialize(list, params) - @list = list - @options = params - end - - def item(&block) - item = Item.new(@options) - dsl = ItemDSL.new(item, @options) - dsl.instance_eval(&block) - @list.items << item - item.list.items.each do |x| - x.prefix item.name - @list.items << x - end - end - end -end diff --git a/lib/prospectus/loader.rb b/lib/prospectus/loader.rb deleted file mode 100644 index bf52a00..0000000 --- a/lib/prospectus/loader.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Prospectus - DEFAULT_FILE = './.prospectus'.freeze - - ## - # Helper for loading prospectus from the current directory - class Loader - def initialize(params = {}) - @options = params - @file = params[:file] || DEFAULT_FILE - @dir = @file + '.d' - end - - def load - return run_file(@options, @file) if File.exist? @file - raise("No #{@file}/#{@dir} found") unless Dir.exist? @dir - files = Dir.glob(@dir + '/*') - raise('No files in ' + @dir) if files.empty? - files.map { |x| run_file(@options, x, true) }.flatten - end - - private - - def run_file(params, file, suffix_file = false) - options = { file: file, suffix_file: suffix_file }.merge(params) - Prospectus.load_from_file(options).check - rescue RuntimeError - puts "Failed parsing #{Dir.pwd}/#{file}" - raise - end - end -end diff --git a/lib/prospectus/modules/git_hash.rb b/lib/prospectus/modules/git_hash.rb deleted file mode 100644 index 661bf1c..0000000 --- a/lib/prospectus/modules/git_hash.rb +++ /dev/null @@ -1,24 +0,0 @@ -module LogCabin - module Modules - ## - # Pull state from a git hash - module GitHash - include Prospectus.helpers.find(:chdir) - - def load! - chdir_helper do - short_arg = @long ? '' : '--short' - hash = `git rev-parse #{short_arg} HEAD 2>/dev/null`.chomp - raise('No hash found') if hash.empty? - @state.value = hash - end - end - - private - - def long - @long = true - end - end - end -end diff --git a/lib/prospectus/modules/git_tag.rb b/lib/prospectus/modules/git_tag.rb deleted file mode 100644 index ef4dafe..0000000 --- a/lib/prospectus/modules/git_tag.rb +++ /dev/null @@ -1,15 +0,0 @@ -module LogCabin - module Modules - ## - # Pull state from a git tag - module GitTag - include Prospectus.helpers.find(:regex) - - def load! - tag = `git describe --tags --abbrev=0 2>/dev/null`.chomp - raise('No tags found') if tag.empty? - @state.value = regex_helper(tag) - end - end - end -end diff --git a/lib/prospectus/modules/github_hash.rb b/lib/prospectus/modules/github_hash.rb deleted file mode 100644 index 58cc12c..0000000 --- a/lib/prospectus/modules/github_hash.rb +++ /dev/null @@ -1,29 +0,0 @@ -module LogCabin - module Modules - ## - # Pull state from the latest GitHub commit - module GithubHash - include Prospectus.helpers.find(:github_api) - - def load! - raise('No repo specified') unless @repo - @branch ||= 'master' - @state.value = @long ? hash : hash.slice(0, 7) - end - - private - - def hash - @hash ||= github_api.branch(@repo, @branch).commit.sha - end - - def branch(value) - @branch = value - end - - def long - @long = true - end - end - end -end diff --git a/lib/prospectus/modules/github_release.rb b/lib/prospectus/modules/github_release.rb deleted file mode 100644 index 1ecd8b8..0000000 --- a/lib/prospectus/modules/github_release.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'json' -require 'open-uri' - -module LogCabin - module Modules - ## - # Pull state from a GitHub release - module GithubRelease - include Prospectus.helpers.find(:regex) - include Prospectus.helpers.find(:github_api) - include Prospectus.helpers.find(:filter) - - def load! - raise('No repo specified') unless @repo - @state.value = regex_helper(release) - end - - private - - def allow_prerelease - @allow_prerelease = true - end - - def release - return @release if @release - releases = github_api.releases(@repo) - releases.reject!(&:draft) - releases.reject!(&:prerelease) unless @allow_prerelease - @release = filter_helper(releases.map(&:tag_name)).first - end - end - end -end diff --git a/lib/prospectus/modules/github_tag.rb b/lib/prospectus/modules/github_tag.rb deleted file mode 100644 index cdb8d4b..0000000 --- a/lib/prospectus/modules/github_tag.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'json' -require 'open-uri' - -module LogCabin - module Modules - ## - # Pull state from a GitHub tag - module GithubTag - include Prospectus.helpers.find(:regex) - include Prospectus.helpers.find(:github_api) - include Prospectus.helpers.find(:filter) - - def load! - raise('No repo specified') unless @repo - @state.value = regex_helper(tag) - end - - private - - def tag - return @tag if @tag - @tags = filter_helper(github_api.tags(@repo).map { |x| x[:name] }).first - end - end - end -end diff --git a/lib/prospectus/modules/gitlab_tag.rb b/lib/prospectus/modules/gitlab_tag.rb deleted file mode 100644 index d71ab25..0000000 --- a/lib/prospectus/modules/gitlab_tag.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'json' -require 'open-uri' - -module LogCabin - module Modules - ## - # Pull state from a Gitlab tag - module GitlabTag - include Prospectus.helpers.find(:regex) - include Prospectus.helpers.find(:gitlab_api) - include Prospectus.helpers.find(:filter) - - def load! - raise('No repo specified') unless @repo - @state.value = regex_helper(tag) - end - - private - - def tags - @tags ||= gitlab_api.tags(@repo).sort do |*points| - dates = points.map { |x| Date.parse(x.commit.committed_date) } - dates.last <=> dates.first - end.map(&:name) - end - - def tag - @tag = filter_helper(tags).first - end - end - end -end diff --git a/lib/prospectus/modules/grep.rb b/lib/prospectus/modules/grep.rb deleted file mode 100644 index 785dd0e..0000000 --- a/lib/prospectus/modules/grep.rb +++ /dev/null @@ -1,30 +0,0 @@ -module LogCabin - module Modules - ## - # Pull state from a local file - module Grep - include Prospectus.helpers.find(:regex) - - def load! - raise('No file specified') unless @file - @find ||= '.*' - line = read_file - @state.value = regex_helper(line) - end - - private - - def read_file - File.read(@file).each_line do |line| - line = line.chomp - return line if line.match(@find) - end - raise("No lines in #{@file} matched #{@find}") - end - - def file(value) - @file = value - end - end - end -end diff --git a/lib/prospectus/modules/homebrew_cask.rb b/lib/prospectus/modules/homebrew_cask.rb deleted file mode 100644 index 26e4442..0000000 --- a/lib/prospectus/modules/homebrew_cask.rb +++ /dev/null @@ -1,20 +0,0 @@ -module LogCabin - module Modules - ## - # Pull state from a homebrew cask file - module HomebrewCask - def load! - raise('No name specified') unless @name - cask_file = "Casks/#{@name}.rb" - output = `brew cask _stanza version #{cask_file}` - @state.value = output.strip - end - - private - - def name(value) - @name = value - end - end - end -end diff --git a/lib/prospectus/modules/homebrew_formula.rb b/lib/prospectus/modules/homebrew_formula.rb deleted file mode 100644 index 27a2110..0000000 --- a/lib/prospectus/modules/homebrew_formula.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'json' - -module LogCabin - module Modules - ## - # Pull state from a homebrew formula file - module HomebrewFormula - def load! - raise('No name specified') unless @name - cask_file = "Formula/#{@name}.rb" - output = `brew info --json=v1 #{cask_file}` - @state.value = JSON.parse(output).first.dig('versions', 'stable') - end - - private - - def name(value) - @name = value - end - end - end -end diff --git a/lib/prospectus/modules/static.rb b/lib/prospectus/modules/static.rb deleted file mode 100644 index e16549d..0000000 --- a/lib/prospectus/modules/static.rb +++ /dev/null @@ -1,18 +0,0 @@ -module LogCabin - module Modules - ## - # Simple text class, uses "set 'value'" to declare value - module Static - def load! - raise('Must use `set` to provide a value') unless @value - @state.value = @value - end - - private - - def set(value) - @value = value - end - end - end -end diff --git a/lib/prospectus/modules/url_xpath.rb b/lib/prospectus/modules/url_xpath.rb deleted file mode 100644 index 4329b09..0000000 --- a/lib/prospectus/modules/url_xpath.rb +++ /dev/null @@ -1,40 +0,0 @@ -Prospectus.extra_dep('url_xpath', 'nokogiri') - -require 'open-uri' - -module LogCabin - module Modules - ## - # Pull state from a GitHub tag - module UrlXpath - include Prospectus.helpers.find(:regex) - - def load! - raise('No url provided') unless @url - raise('No xpath provided') unless @xpath - text = parse_page - @state.value = regex_helper(text) - end - - private - - def parse_page - page = open(@url, @headers || {}) # rubocop:disable Security/Open - html = Nokogiri::HTML(page) { |config| config.strict.nonet } - html.xpath(@xpath).text.strip - end - - def url(value) - @url = value - end - - def xpath(value) - @xpath = value - end - - def headers(value) - @headers = value - end - end - end -end diff --git a/lib/prospectus/state.rb b/lib/prospectus/state.rb deleted file mode 100644 index b1fd677..0000000 --- a/lib/prospectus/state.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Prospectus - ## - # Define a state object that supports modular checks - class State - attr_accessor :value - - def initialize(params = {}) - @options = params - end - - def self.from_block(params = {}, state = nil, &block) - state ||= State.new(params) - dsl = StateDSL.new(state, params) - dsl.instance_eval(&block) - dsl.load! - state - end - - def =~(other) - return super unless other.is_a? Prospectus::State - ov = other.value - return ov.include?(@value) if ov.is_a? Enumerable - return @value =~ ov if ov.is_a? Regexp - @value == ov - end - - def to_s - @value.to_s - end - end - - ## - # DSL for wrapping eval of states - class StateDSL - def initialize(state, params) - @state = state - @options = params - end - - def respond_to_missing?(method, _ = false) - return super if @module - Prospectus.modules.find(method) - true - rescue RuntimeError - super - end - - def method_missing(method, *args, &block) - return super if @module - @module = Prospectus.modules.find(method) - return super unless @module - extend @module - end - end -end diff --git a/lib/prospectus/version.rb b/lib/prospectus/version.rb deleted file mode 100644 index b78b43d..0000000 --- a/lib/prospectus/version.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -## -# Declare package version -module Prospectus - VERSION = '0.9.0'.freeze -end diff --git a/v2/main.go b/main.go similarity index 78% rename from v2/main.go rename to main.go index 96626b4..049e3d3 100644 --- a/v2/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/akerl/prospectus/cmd" + "github.com/akerl/prospectus/v2/cmd" ) func main() { diff --git a/meta b/meta new file mode 160000 index 0000000..2e846e5 --- /dev/null +++ b/meta @@ -0,0 +1 @@ +Subproject commit 2e846e5ea5f52e87394e01384e951a912ad871db diff --git a/plugin/attribute.go b/plugin/attribute.go new file mode 100644 index 0000000..63f850d --- /dev/null +++ b/plugin/attribute.go @@ -0,0 +1,56 @@ +package plugin + +import ( + "fmt" + "strings" +) + +// Attribute defines a single check that is ready for execution +type Attribute struct { + Dir string `json:"dir"` + File string `json:"file"` + Name string `json:"name"` + Metadata map[string]string `json:"metadata"` +} + +// AttributeSet defines a group of Attributes +type AttributeSet []Attribute + +// String returns the Result as a human-readable string +func (a Attribute) String() string { + return fmt.Sprintf( + "%s::%s", + a.Dir, + a.Name, + ) +} + +// String returns the AttributeSet as a human-readable string +func (as AttributeSet) String() string { + var b strings.Builder + for _, item := range as { + b.WriteString(item.String()) + b.WriteString("\n") + } + return b.String() +} + +// Check runs the Attribute and returns Results +func (a Attribute) Check() Result { + r := Result{} + err := call(a.File, "check", a, &r) + if err != nil { + r = NewErrorResult(fmt.Sprintf("execution error: %s", err)) + } + r.Attribute = a + return r +} + +// Check returns the Results from a AttributeSet by calling Execute on each Attribute +func (as AttributeSet) Check() ResultSet { + resultSet := make(ResultSet, len(as)) + for index, item := range as { + resultSet[index] = item.Check() + } + return resultSet +} diff --git a/plugin/loadinput.go b/plugin/loadinput.go new file mode 100644 index 0000000..7bf6727 --- /dev/null +++ b/plugin/loadinput.go @@ -0,0 +1,77 @@ +package plugin + +import ( + "io/ioutil" + "path/filepath" +) + +const ( + prospectusDirName = ".prospectus.d" +) + +// LoadInput defines the input passed to a plugin to load checks +type LoadInput struct { + Dir string `json:"dir"` + File string `json:"file"` +} + +// Load returns an AttributeSet for the provided directory/file +func (l LoadInput) Load() AttributeSet { + cs := AttributeSet{} + err := call(l.File, "load", l, &cs) + if err != nil { + cs = AttributeSet{Attribute{Name: "__failure_to_load__"}} + } + for index := range cs { + cs[index].Dir = l.Dir + cs[index].File = l.File + } + return cs +} + +// NewSet returns a AttributeSet based on a provided list of directories +func NewSet(relativeDirs []string) (AttributeSet, error) { + var err error + + dirs := make([]string, len(relativeDirs)) + for index, item := range relativeDirs { + dirs[index], err = filepath.Abs(item) + if err != nil { + return AttributeSet{}, err + } + } + + as := AttributeSet{} + for _, item := range dirs { + newSet, err := newSetFromDir(item) + if err != nil { + return AttributeSet{}, err + } + as = append(as, newSet...) + } + + return as, nil +} + +func newSetFromDir(absoluteDir string) (AttributeSet, error) { + prospectusDir := filepath.Join(absoluteDir, prospectusDirName) + + fileObjs, err := ioutil.ReadDir(prospectusDir) + if err != nil { + return AttributeSet{}, err + } + + var as AttributeSet + for _, fileObj := range fileObjs { + file := filepath.Join(prospectusDir, fileObj.Name()) + newSet := newSetFromFile(absoluteDir, file) + as = append(as, newSet...) + } + + return as, nil +} + +func newSetFromFile(dir, file string) AttributeSet { + input := LoadInput{Dir: dir, File: file} + return input.Load() +} diff --git a/plugin/main.go b/plugin/main.go new file mode 100644 index 0000000..ee521a1 --- /dev/null +++ b/plugin/main.go @@ -0,0 +1,118 @@ +package plugin + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/akerl/timber/v2/log" + "github.com/ghodss/yaml" +) + +// TODO: add parallelization +// TODO: add logging + +var mainLogger = log.NewLogger("prospectus") +var pluginLogger = log.NewLogger("prospectus:plugin") + +// Plugin defines a Golang plugin object for prospectus request handling +type Plugin interface { + GetConfigPointer() interface{} + Load(LoadInput) AttributeSet + Check(Attribute) Result + Fix(Result) Result +} + +// Start runs a plugin +func Start(p Plugin) error { + err := preflightChecks() + if err != nil { + return err + } + + configFile := os.Args[1] + subcommand := os.Args[2] + + err = loadPluginConfig(configFile, p) + if err != nil { + return err + } + + inputMsg, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return err + } + + output, err := runSubcommand(subcommand, inputMsg, p) + if err != nil { + return err + } + + outputMsg, err := writeMessage(output) + if err != nil { + return err + } + + fmt.Print(string(outputMsg)) + return nil +} + +func runSubcommand(subcommand string, inputMsg []byte, p Plugin) (interface{}, error) { + var output interface{} + + switch subcommand { + case "load": + input := LoadInput{} + if err := readMessage(inputMsg, &input); err != nil { + return output, err + } + output = p.Load(input) + case "check": + input := Attribute{} + if err := readMessage(inputMsg, &input); err != nil { + return output, err + } + output = p.Check(input) + case "fix": + input := Result{} + if err := readMessage(inputMsg, &input); err != nil { + return output, err + } + output = p.Fix(input) + default: + return output, fmt.Errorf("unexpected command provided: %s", subcommand) + } + + return output, nil +} + +func preflightChecks() error { + if len(os.Args) != 3 { + return fmt.Errorf("unexpected number of args provided: %d", len(os.Args)) + } + + info, err := os.Stdin.Stat() + if err != nil { + return err + } + + if info.Mode()&os.ModeNamedPipe != os.ModeNamedPipe || info.Size() <= 0 { + return fmt.Errorf("plugin executed without stdin") + } + + return nil +} + +func loadPluginConfig(configFile string, p Plugin) error { + output := p.GetConfigPointer() + fileInfo, err := os.Stat(configFile) + if os.IsNotExist(err) { + return fmt.Errorf("config file does not exist") + } + if fileInfo.IsDir() { + return fmt.Errorf("config file is a directory") + } + + data, err := ioutil.ReadFile(configFile) + return yaml.Unmarshal(data, output) +} diff --git a/plugin/message.go b/plugin/message.go new file mode 100644 index 0000000..7548a0a --- /dev/null +++ b/plugin/message.go @@ -0,0 +1,73 @@ +package plugin + +import ( + "bytes" + "encoding/json" + "fmt" + "os/exec" +) + +const ( + apiVersion = 1 +) + +type message struct { + Version int `json:"version"` + Contents json.RawMessage `json:"contents"` +} + +func writeMessage(input interface{}) ([]byte, error) { + contents, err := json.Marshal(input) + if err != nil { + return []byte{}, err + } + m := message{ + Version: apiVersion, + Contents: contents, + } + return json.Marshal(m) +} + +func readMessage(input []byte, output interface{}) error { + var m message + err := json.Unmarshal(input, &m) + if err != nil { + return err + } + if m.Version != apiVersion { + return fmt.Errorf( + "plugin version mismatch: %d (expected) vs %d (actual)", + apiVersion, + m.Version, + ) + } + return json.Unmarshal(m.Contents, output) +} + +func call(file, command string, input interface{}, output interface{}) error { + cmd := exec.Command(file, command) + + inputBytes, err := writeMessage(input) + if err != nil { + return err + } + + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + stdin.Write(inputBytes) + stdin.Close() + + var stdoutBytes bytes.Buffer + var stderrBytes bytes.Buffer + + cmd.Stdout = &stdoutBytes + cmd.Stderr = &stderrBytes + err = cmd.Run() + if err != nil { + return fmt.Errorf("%s: %s", err, stderrBytes.String()) + } + + return readMessage(stdoutBytes.Bytes(), output) +} diff --git a/plugin/result.go b/plugin/result.go new file mode 100644 index 0000000..56720df --- /dev/null +++ b/plugin/result.go @@ -0,0 +1,69 @@ +package plugin + +import ( + "fmt" + "strings" +) + +// Result defines the results of executing a Attribute +type Result struct { + Actual string `json:"actual"` + Expected string `json:"expected"` + Matches bool `json:"matches"` + Attribute Attribute `json:"attribute"` +} + +// ResultSet defines a group of Results +type ResultSet []Result + +// String returns the Result as a human-readable string +func (r Result) String() string { + return fmt.Sprintf( + "%s: %s / %s", + r.Attribute, + r.Actual, + r.Expected, + ) +} + +// String returns the ResultSet as a human-readable string +func (rs ResultSet) String() string { + var b strings.Builder + for _, item := range rs { + b.WriteString(item.String()) + b.WriteString("\n") + } + return b.String() +} + +// Fix attempts to resolve a mismatched expectation +func (r Result) Fix() Result { + if r.Matches { + return r + } + newResult := Result{} + err := call(r.Attribute.File, "fix", r, &newResult) + if err != nil { + newResult = NewErrorResult(fmt.Sprintf("fix error: %s", err)) + } + newResult.Attribute = r.Attribute + return newResult +} + +// Fix attempts to fix all results in a ResultSet +func (rs ResultSet) Fix() ResultSet { + newResultSet := make(ResultSet, len(rs)) + for index, item := range rs { + newResultSet[index] = item.Fix() + } + return newResultSet +} + +// NewErrorResult creates an error result from a given string +func NewErrorResult(msg string) Result { + return Result{ + Actual: "error", + Expected: msg, + Matches: false, + } +} diff --git a/prospectus.gemspec b/prospectus.gemspec deleted file mode 100644 index 936193d..0000000 --- a/prospectus.gemspec +++ /dev/null @@ -1,30 +0,0 @@ -require 'English' -$LOAD_PATH.unshift File.expand_path('lib', __dir__) -require 'prospectus/version' - -Gem::Specification.new do |s| - s.name = 'prospectus' - s.version = Prospectus::VERSION - s.date = Time.now.strftime('%Y-%m-%d') - - s.summary = 'Tool and DSL for checking expected vs actual state' - s.description = 'Tool and DSL for checking expected vs actual state' - s.authors = ['Les Aker'] - s.email = 'me@lesaker.org' - s.homepage = 'https://github.com/akerl/prospectus' - s.license = 'MIT' - - s.files = `git ls-files`.split - s.test_files = `git ls-files spec/*`.split - s.executables = ['prospectus'] - - s.add_dependency 'logcabin', '~> 0.1.3' - s.add_dependency 'mercenary', '~> 0.3.4' - - s.add_development_dependency 'codecov', '~> 0.1.1' - s.add_development_dependency 'fuubar', '~> 2.5.0' - s.add_development_dependency 'goodcop', '~> 0.8.0' - s.add_development_dependency 'rake', '~> 13.0.0' - s.add_development_dependency 'rspec', '~> 3.9.0' - s.add_development_dependency 'rubocop', '~> 0.76.0' -end diff --git a/spec/prospectus_spec.rb b/spec/prospectus_spec.rb deleted file mode 100644 index f8ec369..0000000 --- a/spec/prospectus_spec.rb +++ /dev/null @@ -1 +0,0 @@ -require 'spec_helper' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index a52f00b..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -if ENV['CI'] == 'true' - require 'simplecov' - require 'codecov' - SimpleCov.formatter = SimpleCov::Formatter::Codecov - SimpleCov.start do - add_filter '/spec/' - end -end - -require 'rspec' -require 'prospectus' diff --git a/v2/.gitignore b/v2/.gitignore deleted file mode 100644 index b07d682..0000000 --- a/v2/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/.gopath -/bin -/vendor/ -/pkg/ -/.github -/payload.zip -/.tools/ -/Dockerfile -prospectus diff --git a/v2/.prospectus b/v2/.prospectus deleted file mode 100644 index 0b4bdba..0000000 --- a/v2/.prospectus +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env ruby - -require 'prospectus' - -Prospectus.extra_dep('file', 'prospectus_travis') -Prospectus.extra_dep('file', 'prospectus_golang') - -item do - noop - - extend ProspectusGolang::Deps.new - extend ProspectusTravis::Build.new('akerl/prospectus-ng') -end diff --git a/v2/.travis.yml b/v2/.travis.yml deleted file mode 100644 index c6bae03..0000000 --- a/v2/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: required -dist: xenial -services: -- docker -env: - global: - - PKGFORGE_STATEFILE=/tmp/pkgforge -script: make -deploy: - provider: script - script: make release || travis_terminate 1 - skip_cleanup: true - on: - tags: true -notifications: - email: false diff --git a/v2/LICENSE b/v2/LICENSE deleted file mode 100644 index 74b636a..0000000 --- a/v2/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2019 Les Aker - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - diff --git a/v2/README.md b/v2/README.md deleted file mode 100644 index 1e06c24..0000000 --- a/v2/README.md +++ /dev/null @@ -1,43 +0,0 @@ -prospectus-ng -========= - -[![Build Status](https://img.shields.io/travis/com/akerl/prospectus-ng.svg)](https://travis-ci.com/akerl/prospectus) -[![GitHub release](https://img.shields.io/github/release/akerl/prospectus-ng.svg)](https://github.com/akerl/prospectus-ng/releases) -[![MIT Licensed](https://img.shields.io/badge/license-MIT-green.svg)](https://tldrlegal.com/license/mit-license) - -Tool to check for changes in expected vs. actual state - -## Usage - -### Check specification - -Checks must implement responses for the following commands: - -### load - -The `load` command accepts a hash with a single key, the directory being checked, and returns an array of checks with optional metadata. - -Input: `{"dir": "/path/to/main/dir"}` -Output: `[{"name": "check_N", "metadata": {"foo": "bar"}}, ...]` - -### execute - -The `execute` command accepts a hash representing the check object. Metadata provided during the `load` call is included. The return value must be a Result object for the given check. - -Input: `{"dir": "/path/to/main/dir", "file": "/path/to/main/dir/.prospectus.d/checkfile", "name": "check_N", "metadata": {"foo": "bar"}}` -Output: `{"actual": "unhappy", "expected": {"type": "string", "data": {"expected": "happy"}}}` - -### fix - -The `fix` command can attempt to fix a failed check automatically. It accepts a hash representing the failed result, which includes the originating check. The return value must be a Result object for the given check. - -**Note:** The check must respond to the `fix` command, but if it does not support automatic fixes, it can respond by emiting the same result object it was given. - -Input: `{"actual": "unhappy", "expected": {"type": "string", "data": {"expected": "happy"}}, "check": {"dir": "/path/to/main/dir", "file": "/path/to/main/dir/.prospectus.d/checkfile", "name": "check_N", "metadata": {"foo": "bar"}}}` -Output: `{"actual": "happy", "expected": {"type": "string", "data": {"expected": "happy"}}}` - -## Installation - -## License - -prospectus-ng is released under the MIT License. See the bundled LICENSE file for details. diff --git a/v2/checks/main.go b/v2/checks/main.go deleted file mode 100644 index ffd443d..0000000 --- a/v2/checks/main.go +++ /dev/null @@ -1,221 +0,0 @@ -package checks - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os/exec" - "path/filepath" - "strings" - - "github.com/akerl/prospectus/expectations" -) - -const ( - prospectusDirName = ".prospectus.d" -) - -// TODO: add timber logging -// TODO: add parallelization - -// Check defines a single check that is ready for execution -type Check struct { - Dir string `json:"dir"` - File string `json:"file"` - Name string `json:"name"` - Metadata map[string]string `json:"metadata"` -} - -// CheckSet defines a group of Checks -type CheckSet []Check - -// Result defines the results of executing a Check -type Result struct { - Actual string `json:"actual"` - Expected expectations.Wrapper `json:"expected"` - Check Check `json:"check"` -} - -// ResultSet defines a group of Results -type ResultSet []Result - -type loadCheckInput struct { - Dir string `json:"dir"` -} - -// NewSet returns a CheckSet based on a provided list of directories -func NewSet(relativeDirs []string) (CheckSet, error) { - var err error - - dirs := make([]string, len(relativeDirs)) - for index, item := range relativeDirs { - dirs[index], err = filepath.Abs(item) - if err != nil { - return CheckSet{}, err - } - } - - var cs CheckSet - for _, item := range dirs { - newSet, err := newSetFromDir(item) - if err != nil { - return CheckSet{}, err - } - cs = append(cs, newSet...) - } - - return cs, nil -} - -func newSetFromDir(absoluteDir string) (CheckSet, error) { - prospectusDir := filepath.Join(absoluteDir, prospectusDirName) - - fileObjs, err := ioutil.ReadDir(prospectusDir) - if err != nil { - return CheckSet{}, err - } - - var cs CheckSet - for _, fileObj := range fileObjs { - file := filepath.Join(prospectusDir, fileObj.Name()) - newSet, err := newSetFromFile(absoluteDir, file) - if err != nil { - return CheckSet{}, err - } - cs = append(cs, newSet...) - } - - return cs, nil -} - -func newSetFromFile(dir, file string) (CheckSet, error) { - cs := CheckSet{} - input := loadCheckInput{Dir: dir} - err := execProspectusFile(file, "load", input, &cs) - if err != nil { - return CheckSet{}, fmt.Errorf("Failed loading %s: %s", file, err) - } - for index := range cs { - cs[index].Dir = dir - cs[index].File = file - } - return cs, nil -} - -func execProspectusFile(file, command string, input interface{}, output interface{}) error { - cmd := exec.Command(file, command) - - inputBytes, err := json.Marshal(input) - if err != nil { - return err - } - - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - stdin.Write(inputBytes) - stdin.Close() - - stdout, err := cmd.Output() - if err != nil { - return err - } - - return json.Unmarshal(stdout, output) -} - -// Execute returns the Results from a CheckSet by calling Execute on each Check -func (cs CheckSet) Execute() ResultSet { - resultSet := make(ResultSet, len(cs)) - for index, item := range cs { - resultSet[index] = item.Execute() - } - return resultSet -} - -// Execute runs the Check and returns Results -func (c Check) Execute() Result { - return execProspectusForResult("execute", c, c) -} - -func execProspectusForResult(method string, c Check, input interface{}) Result { - r := Result{} - err := execProspectusFile(c.File, method, input, &r) - if err != nil { - return NewErrorResult(fmt.Sprintf("%s error: %s", method, err), c) - } - r.Check = c - return r -} - -// String returns the Result as a human-readable string -func (c Check) String() string { - return fmt.Sprintf( - "%s::%s", - c.Dir, - c.Name, - ) -} - -// Changed filters a ResultSet to only Results which do not match -func (rs ResultSet) Changed() ResultSet { - var newResultSet ResultSet - for _, item := range rs { - if !item.Matches() { - newResultSet = append(newResultSet, item) - } - } - return newResultSet -} - -// Matches returns true if the Expected and Actual values of the Result match -func (r Result) Matches() bool { - return r.Expected.Matches(r.Actual) -} - -// JSON returns the ResultsSet as a marshalled JSON string -func (rs ResultSet) JSON() (string, error) { - data, err := json.MarshalIndent(rs, "", " ") - if err != nil { - return "", err - } - return string(data), nil -} - -// String returns the ResultsSet as a human-readable string -func (rs ResultSet) String() string { - var b strings.Builder - for _, item := range rs { - b.WriteString(item.String()) - b.WriteString("\n") - } - return b.String() -} - -// String returns the Result as a human-readable string -func (r Result) String() string { - return fmt.Sprintf( - "%s: %s / %s", - r.Check, - r.Actual, - r.Expected.String(), - ) -} - -// Fix attempts to resolve a mismatched expectation -func (r Result) Fix() Result { - return execProspectusForResult("fix", r.Check, r) -} - -// NewErrorResult creates an error result from a given string -func NewErrorResult(msg string, c Check) Result { - return Result{ - Actual: "error", - Expected: expectations.Wrapper{ - Type: "error", - Data: map[string]string{"msg": msg}, - }, - Check: c, - } -} diff --git a/v2/cmd/fix.go b/v2/cmd/fix.go deleted file mode 100644 index b992344..0000000 --- a/v2/cmd/fix.go +++ /dev/null @@ -1,78 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/akerl/prospectus/checks" - - "github.com/spf13/cobra" -) - -var fixCmd = &cobra.Command{ - Use: "fix", - Short: "Attempt to fix items where expected state differs from actual state", - RunE: fixRunner, -} - -func init() { - rootCmd.AddCommand(fixCmd) - f := fixCmd.Flags() - f.Bool("json", false, "Print output as JSON") -} - -func fixRunner(cmd *cobra.Command, args []string) error { - flags := cmd.Flags() - flagJSON, err := flags.GetBool("json") - if err != nil { - return err - } - - params := []string{"."} - if len(args) != 0 { - params = args - } - - cs, err := checks.NewSet(params) - if err != nil { - return err - } - results := cs.Execute() - - fixResults := map[string]checks.ResultSet{ - "fixed": {}, - "unfixed": {}, - "good": {}, - } - for _, item := range results { - if item.Matches() { - fixResults["good"] = append(fixResults["good"], item) - } else { - newResult := item.Fix() - if newResult.Matches() { - fixResults["fixed"] = append(fixResults["fixed"], newResult) - } else { - fixResults["unfixed"] = append(fixResults["unfixed"], newResult) - } - } - } - - var output strings.Builder - if flagJSON { - outputBytes, err := json.MarshalIndent(fixResults, "", " ") - if err != nil { - return err - } - output.Write(outputBytes) - } else { - for _, key := range []string{"good", "fixed", "unfixed"} { - output.WriteString(fmt.Sprintf("%s:\n", key)) - for _, item := range fixResults[key] { - output.WriteString(fmt.Sprintf(" %s\n", item)) - } - } - } - fmt.Println(output.String()) - return nil -} diff --git a/v2/expectations/error.go b/v2/expectations/error.go deleted file mode 100644 index b1a5220..0000000 --- a/v2/expectations/error.go +++ /dev/null @@ -1,25 +0,0 @@ -package expectations - -import ( - "fmt" -) - -type errorExpectation struct { - msg string -} - -// Load creates a new error expectation -func (e *errorExpectation) Load(data map[string]string) Expectation { - e.msg = data["msg"] - return e -} - -// Matches returns false in all cases -func (e *errorExpectation) Matches(_ string) bool { - return false -} - -// String returns a type error message -func (e *errorExpectation) String() string { - return fmt.Sprintf("error: %s", e.msg) -} diff --git a/v2/expectations/main.go b/v2/expectations/main.go deleted file mode 100644 index 8450c55..0000000 --- a/v2/expectations/main.go +++ /dev/null @@ -1,54 +0,0 @@ -package expectations - -import ( - "fmt" -) - -// Expectation defines a pluggable interface for matching desired state to actual -type Expectation interface { - Load(map[string]string) Expectation - Matches(string) bool - String() string -} - -// Wrapper defines the parameters that construct an Expectation -type Wrapper struct { - Type string `json:"type"` - Data map[string]string `json:"data"` - expectation Expectation -} - -// Matches proxies the request to the underlying expectation -func (w Wrapper) Matches(actual string) bool { - if w.expectation == nil { - w.load() - } - return w.expectation.Matches(actual) -} - -// String proxies the request to the underlying expectation -func (w Wrapper) String() string { - if w.expectation == nil { - w.load() - } - return w.expectation.String() -} - -func (w *Wrapper) load() { - itemFunc, ok := types[w.Type] - if !ok { - itemFunc = types["error"] - w.Data["msg"] = fmt.Sprintf("expectation type not known: %s", w.Type) - } - e := itemFunc() - w.expectation = e.Load(w.Data) -} - -type builder func() Expectation - -var types = map[string]builder{ - "error": func() Expectation { return &errorExpectation{} }, - "string": func() Expectation { return &stringExpectation{} }, - "regex": func() Expectation { return ®exExpectation{} }, - "set": func() Expectation { return &setExpectation{} }, -} diff --git a/v2/expectations/regex.go b/v2/expectations/regex.go deleted file mode 100644 index b09017c..0000000 --- a/v2/expectations/regex.go +++ /dev/null @@ -1,35 +0,0 @@ -package expectations - -import ( - "fmt" - "regexp" -) - -type regexExpectation struct { - regex *regexp.Regexp - raw string -} - -// Load creates a new regex expectation -func (r *regexExpectation) Load(data map[string]string) Expectation { - var err error - r.raw = data["pattern"] - r.regex, err = regexp.Compile(r.raw) - if err != nil { - e := errorExpectation{} - return e.Load(map[string]string{ - "msg": fmt.Sprintf("invalid regex: %s (%s)", r.raw, err), - }) - } - return r -} - -// Matches returns true if the actual value exists in the expected regex -func (r *regexExpectation) Matches(actual string) bool { - return r.regex.MatchString(actual) -} - -// String returns the original string with separators intact -func (r *regexExpectation) String() string { - return r.raw -} diff --git a/v2/expectations/set.go b/v2/expectations/set.go deleted file mode 100644 index 817e263..0000000 --- a/v2/expectations/set.go +++ /dev/null @@ -1,37 +0,0 @@ -package expectations - -import ( - "strings" -) - -type setExpectation struct { - expected []string - separator string - raw string -} - -// Load creates a new set expectation -func (s *setExpectation) Load(data map[string]string) Expectation { - s.separator = data["separator"] - if s.separator == "" { - s.separator = "," - } - s.raw = data["expected"] - s.expected = strings.Split(s.raw, s.separator) - return s -} - -// Matches returns true if the actual value exists in the expected set -func (s *setExpectation) Matches(actual string) bool { - for _, item := range s.expected { - if item == actual { - return true - } - } - return false -} - -// String returns the original string with separators intact -func (s *setExpectation) String() string { - return s.raw -} diff --git a/v2/expectations/string.go b/v2/expectations/string.go deleted file mode 100644 index 08983ef..0000000 --- a/v2/expectations/string.go +++ /dev/null @@ -1,21 +0,0 @@ -package expectations - -type stringExpectation struct { - expected string -} - -// Load creates a new string expectation -func (s *stringExpectation) Load(data map[string]string) Expectation { - s.expected = data["expected"] - return s -} - -// Matches returns true if the expected and actual strings are identical -func (s *stringExpectation) Matches(actual string) bool { - return s.expected == actual -} - -// String returns the expected string -func (s *stringExpectation) String() string { - return s.expected -} diff --git a/v2/go.mod b/v2/go.mod deleted file mode 100644 index 54b37bb..0000000 --- a/v2/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/akerl/prospectus - -go 1.12 - -require github.com/spf13/cobra v0.0.5 diff --git a/v2/meta b/v2/meta deleted file mode 160000 index a87ab3c..0000000 --- a/v2/meta +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a87ab3c664faaa259dc310d3eedb78da832b927d