diff --git a/.gitignore b/.gitignore index 7650fdea..da177d95 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ skopeo +skopeo.1 diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 0ab62fdc..cb9cb0d0 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,9 @@ { "ImportPath": "github.com/runcom/skopeo", "GoVersion": "go1.5.3", + "Packages": [ + "." + ], "Deps": [ { "ImportPath": "github.com/Sirupsen/logrus", @@ -22,6 +25,11 @@ "Comment": "v1.4.1-9391-g5537a92", "Rev": "5537a92e450ea56e2002f83ff50bb70fdb2cc25e" }, + { + "ImportPath": "github.com/docker/docker/cliconfig", + "Comment": "v1.4.1-9391-g5537a92", + "Rev": "5537a92e450ea56e2002f83ff50bb70fdb2cc25e" + }, { "ImportPath": "github.com/docker/docker/daemon/graphdriver", "Comment": "v1.4.1-9391-g5537a92", diff --git a/Godeps/_workspace/src/github.com/docker/docker/cliconfig/config.go b/Godeps/_workspace/src/github.com/docker/docker/cliconfig/config.go new file mode 100644 index 00000000..f5b2be8a --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/cliconfig/config.go @@ -0,0 +1,277 @@ +package cliconfig + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/homedir" + "github.com/docker/engine-api/types" +) + +const ( + // ConfigFileName is the name of config file + ConfigFileName = "config.json" + oldConfigfile = ".dockercfg" + + // This constant is only used for really old config files when the + // URL wasn't saved as part of the config file and it was just + // assumed to be this value. + defaultIndexserver = "https://index.docker.io/v1/" +) + +var ( + configDir = os.Getenv("DOCKER_CONFIG") +) + +func init() { + if configDir == "" { + configDir = filepath.Join(homedir.Get(), ".docker") + } +} + +// ConfigDir returns the directory the configuration file is stored in +func ConfigDir() string { + return configDir +} + +// SetConfigDir sets the directory the configuration file is stored in +func SetConfigDir(dir string) { + configDir = dir +} + +// ConfigFile ~/.docker/config.json file info +type ConfigFile struct { + AuthConfigs map[string]types.AuthConfig `json:"auths"` + HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` + PsFormat string `json:"psFormat,omitempty"` + ImagesFormat string `json:"imagesFormat,omitempty"` + DetachKeys string `json:"detachKeys,omitempty"` + filename string // Note: not serialized - for internal use only +} + +// NewConfigFile initializes an empty configuration file for the given filename 'fn' +func NewConfigFile(fn string) *ConfigFile { + return &ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + HTTPHeaders: make(map[string]string), + filename: fn, + } +} + +// LegacyLoadFromReader reads the non-nested configuration data given and sets up the +// auth config information with given directory and populates the receiver object +func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error { + b, err := ioutil.ReadAll(configData) + if err != nil { + return err + } + + if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil { + arr := strings.Split(string(b), "\n") + if len(arr) < 2 { + return fmt.Errorf("The Auth config file is empty") + } + authConfig := types.AuthConfig{} + origAuth := strings.Split(arr[0], " = ") + if len(origAuth) != 2 { + return fmt.Errorf("Invalid Auth config file") + } + authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) + if err != nil { + return err + } + origEmail := strings.Split(arr[1], " = ") + if len(origEmail) != 2 { + return fmt.Errorf("Invalid Auth config file") + } + authConfig.Email = origEmail[1] + authConfig.ServerAddress = defaultIndexserver + configFile.AuthConfigs[defaultIndexserver] = authConfig + } else { + for k, authConfig := range configFile.AuthConfigs { + authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) + if err != nil { + return err + } + authConfig.Auth = "" + authConfig.ServerAddress = k + configFile.AuthConfigs[k] = authConfig + } + } + return nil +} + +// LoadFromReader reads the configuration data given and sets up the auth config +// information with given directory and populates the receiver object +func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error { + if err := json.NewDecoder(configData).Decode(&configFile); err != nil { + return err + } + var err error + for addr, ac := range configFile.AuthConfigs { + ac.Username, ac.Password, err = decodeAuth(ac.Auth) + if err != nil { + return err + } + ac.Auth = "" + ac.ServerAddress = addr + configFile.AuthConfigs[addr] = ac + } + return nil +} + +// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from +// a non-nested reader +func LegacyLoadFromReader(configData io.Reader) (*ConfigFile, error) { + configFile := ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + } + err := configFile.LegacyLoadFromReader(configData) + return &configFile, err +} + +// LoadFromReader is a convenience function that creates a ConfigFile object from +// a reader +func LoadFromReader(configData io.Reader) (*ConfigFile, error) { + configFile := ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + } + err := configFile.LoadFromReader(configData) + return &configFile, err +} + +// Load reads the configuration files in the given directory, and sets up +// the auth config information and return values. +// FIXME: use the internal golang config parser +func Load(configDir string) (*ConfigFile, error) { + if configDir == "" { + configDir = ConfigDir() + } + + configFile := ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + filename: filepath.Join(configDir, ConfigFileName), + } + + // Try happy path first - latest config file + if _, err := os.Stat(configFile.filename); err == nil { + file, err := os.Open(configFile.filename) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", configFile.filename, err) + } + defer file.Close() + err = configFile.LoadFromReader(file) + if err != nil { + err = fmt.Errorf("%s - %v", configFile.filename, err) + } + return &configFile, err + } else if !os.IsNotExist(err) { + // if file is there but we can't stat it for any reason other + // than it doesn't exist then stop + return &configFile, fmt.Errorf("%s - %v", configFile.filename, err) + } + + // Can't find latest config file so check for the old one + confFile := filepath.Join(homedir.Get(), oldConfigfile) + if _, err := os.Stat(confFile); err != nil { + return &configFile, nil //missing file is not an error + } + file, err := os.Open(confFile) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", confFile, err) + } + defer file.Close() + err = configFile.LegacyLoadFromReader(file) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", confFile, err) + } + + if configFile.HTTPHeaders == nil { + configFile.HTTPHeaders = map[string]string{} + } + return &configFile, nil +} + +// SaveToWriter encodes and writes out all the authorization information to +// the given writer +func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { + // Encode sensitive data into a new/temp struct + tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs)) + for k, authConfig := range configFile.AuthConfigs { + authCopy := authConfig + // encode and save the authstring, while blanking out the original fields + authCopy.Auth = encodeAuth(&authCopy) + authCopy.Username = "" + authCopy.Password = "" + authCopy.ServerAddress = "" + tmpAuthConfigs[k] = authCopy + } + + saveAuthConfigs := configFile.AuthConfigs + configFile.AuthConfigs = tmpAuthConfigs + defer func() { configFile.AuthConfigs = saveAuthConfigs }() + + data, err := json.MarshalIndent(configFile, "", "\t") + if err != nil { + return err + } + _, err = writer.Write(data) + return err +} + +// Save encodes and writes out all the authorization information +func (configFile *ConfigFile) Save() error { + if configFile.Filename() == "" { + return fmt.Errorf("Can't save config with empty filename") + } + + if err := os.MkdirAll(filepath.Dir(configFile.filename), 0700); err != nil { + return err + } + f, err := os.OpenFile(configFile.filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer f.Close() + return configFile.SaveToWriter(f) +} + +// Filename returns the name of the configuration file +func (configFile *ConfigFile) Filename() string { + return configFile.filename +} + +// encodeAuth creates a base64 encoded string to containing authorization information +func encodeAuth(authConfig *types.AuthConfig) string { + authStr := authConfig.Username + ":" + authConfig.Password + msg := []byte(authStr) + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) + base64.StdEncoding.Encode(encoded, msg) + return string(encoded) +} + +// decodeAuth decodes a base64 encoded string and returns username and password +func decodeAuth(authStr string) (string, string, error) { + decLen := base64.StdEncoding.DecodedLen(len(authStr)) + decoded := make([]byte, decLen) + authByte := []byte(authStr) + n, err := base64.StdEncoding.Decode(decoded, authByte) + if err != nil { + return "", "", err + } + if n > decLen { + return "", "", fmt.Errorf("Something went wrong decoding auth config") + } + arr := strings.SplitN(string(decoded), ":", 2) + if len(arr) != 2 { + return "", "", fmt.Errorf("Invalid auth configuration file") + } + password := strings.Trim(arr[1], "\x00") + return arr[0], password, nil +} diff --git a/Makefile b/Makefile index 06ef8365..6808641f 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,17 @@ export GOPATH:=$(CURDIR)/Godeps/_workspace:$(GOPATH) -BINDIR=${DESTDIR}/usr/local/bin/ +BINDIR=${DESTDIR}/usr/bin/ +MANDIR=${DESTDIR}/usr/share/man all: go build -o skopeo . + go-md2man -in man/skopeo.1.md -out skopeo.1 install: install -d -m 0755 ${BINDIR} install -m 755 skopeo ${BINDIR} + install -m 644 skopeo.1 ${MANDIR}/man1/ clean: rm -f skopeo + rm -f skopeo.1 diff --git a/README.md b/README.md index 5e4aed9b..1c3d8f93 100644 --- a/README.md +++ b/README.md @@ -92,17 +92,9 @@ $ make ``` Installing - -If you built from the _Building_ step, just do: ```sh $ sudo make install ``` -You can also grab a binary, built for _linux x86_64_, from the releases page, or: -```sh -$ export RELEASE=0.0.1-alpha -$ wget https://github.com/runcom/skopeo/releases/download/v$RELEASE/skopeo -$ chmod +x skopeo -$ sudo mv skopeo /usr/local/bin/skopeo -``` TODO - - show repo tags via flag or when reference isn't tagged or digested diff --git a/main.go b/main.go index 35b2eaf5..79856ae4 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( ) const ( - version = "0.0.1-alpha" + version = "0.1.0" usage = "inspect images on a registry" ) diff --git a/man/skopeo.1.md b/man/skopeo.1.md new file mode 100644 index 00000000..fad4f07c --- /dev/null +++ b/man/skopeo.1.md @@ -0,0 +1,14 @@ +% SKOPEO(1) +% Antonio Murdaca +% JANUARY 2016 +# NAME +skopeo - Inspect Docker images and repositories on registries + +# SYNOPSIS + +# DESCRIPTION + +# ARGUMENTS + +# AUTHORS +Antonio Murdaca diff --git a/skopeo.spec b/skopeo.spec new file mode 100644 index 00000000..df458813 --- /dev/null +++ b/skopeo.spec @@ -0,0 +1,37 @@ +Name: skopeo +Version: 0.1.0 +Release: 1%{?dist} +Summary: Inspect Docker images and repositories on registries +License: MIT +URL: https://github.com/runcom/skopeo +Source: https://github.com/runcom/skopeo/archive/v%{version}.tar.gz + +BuildRequires: golang +BuildRequires: golang-github-cpuguy83-go-md2man + +%global debug_package %{nil} + +%description +Get information about a Docker image or repository without pulling it + +%prep +%setup -q -n %{name}-%{version} + +%build +mkdir -p src/github.com/runcom +ln -s ../../../ src/github.com/runcom/skopeo +export GOPATH=$(pwd):%{gopath} +make %{?_smp_mflags} + +%install +mkdir -p $RPM_BUILD_ROOT/%{_mandir}/man1 +make DESTDIR=%{buildroot} install + +%files +%{_bindir}/skopeo +%{_mandir}/man1/skopeo.1* +%doc README.md LICENSE + +%changelog +* Thu Jan 21 2016 Antonio Murdaca - 0.1.0 +- v0.1.0