diff --git a/internal/hcp/registry/hcl.go b/internal/hcp/registry/hcl.go index 208f4d4374c..e4668ea1d83 100644 --- a/internal/hcp/registry/hcl.go +++ b/internal/hcp/registry/hcl.go @@ -88,8 +88,8 @@ func (h *HCLRegistry) CompleteBuild( buildName = cb.Type } - metadata := cb.GetMetadata() - err := h.bucket.Version.AddMetadataToBuild(ctx, buildName, metadata) + buildMetadata := cb.GetMetadata() + err := h.bucket.Version.AddMetadataToBuild(ctx, buildName, buildMetadata, h.metadata) if err != nil { return nil, err } diff --git a/internal/hcp/registry/json.go b/internal/hcp/registry/json.go index b180f1e2ada..1ce5980d47a 100644 --- a/internal/hcp/registry/json.go +++ b/internal/hcp/registry/json.go @@ -98,7 +98,7 @@ func (h *JSONRegistry) CompleteBuild( ) ([]sdkpacker.Artifact, error) { buildName := build.Name() buildMetadata := build.(*packer.CoreBuild).GetMetadata() - err := h.bucket.Version.AddMetadataToBuild(ctx, buildName, buildMetadata) + err := h.bucket.Version.AddMetadataToBuild(ctx, buildName, buildMetadata, h.metadata) if err != nil { return nil, err } diff --git a/internal/hcp/registry/metadata.cicd.go b/internal/hcp/registry/metadata.cicd.go new file mode 100644 index 00000000000..af1abf2499c --- /dev/null +++ b/internal/hcp/registry/metadata.cicd.go @@ -0,0 +1,103 @@ +package registry + +import ( + "fmt" + "os" +) + +type CICD interface { + Detect() bool + Env() map[string]string + Type() string +} + +type GithubActions struct{} + +func (g *GithubActions) Detect() bool { + _, ok := os.LookupEnv("GITHUB_ACTIONS") + return ok +} + +func (g *GithubActions) Env() map[string]string { + env := make(map[string]string) + keys := []string{ + "GITHUB_REPOSITORY", + "GITHUB_REPOSITORY_ID", + "GITHUB_WORKFLOW_URL", + "GITHUB_SHA", + "GITHUB_REF", + "GITHUB_ACTOR", + "GITHUB_ACTOR_ID", + "GITHUB_TRIGGERING_ACTOR", + "GITHUB_EVENT_NAME", + "GITHUB_JOB", + } + + for _, key := range keys { + if value, ok := os.LookupEnv(key); ok { + env[key] = value + } + } + + env["GITHUB_WORKFLOW_URL"] = fmt.Sprintf("%s/%s/actions/runs/%s", os.Getenv("GITHUB_SERVER_URL"), os.Getenv("GITHUB_REPOSITORY"), os.Getenv("GITHUB_RUN_ID")) + return env +} + +func (g *GithubActions) Type() string { + return "github-actions" +} + +type GitlabCI struct{} + +func (g *GitlabCI) Detect() bool { + _, ok := os.LookupEnv("GITLAB_CI") + return ok +} + +func (g *GitlabCI) Env() map[string]string { + env := make(map[string]string) + keys := []string{ + "CI_PROJECT_NAME", + "CI_PROJECT_ID", + "CI_PROJECT_URL", + "CI_COMMIT_SHA", + "CI_COMMIT_REF_NAME", + "GITLAB_USER_NAME", + "GITLAB_USER_ID", + "CI_PIPELINE_SOURCE", + "CI_PIPELINE_URL", + "CI_JOB_URL", + "CI_SERVER_NAME", + "CI_REGISTRY_IMAGE", + } + + for _, key := range keys { + if value, ok := os.LookupEnv(key); ok { + env[key] = value + } + } + + return env +} + +func (g *GitlabCI) Type() string { + return "gitlab-ci" +} + +func GetCicdMetadata() map[string]interface{} { + cicd := []CICD{ + &GithubActions{}, + &GitlabCI{}, + } + + for _, c := range cicd { + if c.Detect() { + return map[string]interface{}{ + "type": c.Type(), + "details": c.Env(), + } + } + } + + return nil +} diff --git a/internal/hcp/registry/metadata.os.go b/internal/hcp/registry/metadata.os.go new file mode 100644 index 00000000000..eb262cec883 --- /dev/null +++ b/internal/hcp/registry/metadata.os.go @@ -0,0 +1,129 @@ +package registry + +import ( + "bytes" + "log" + "os/exec" + "runtime" + "strings" + "time" +) + +type OSInfo struct { + Name string + Arch string + Version string +} + +func GetOSMetadata() map[string]interface{} { + var osInfo OSInfo + + switch runtime.GOOS { + case "windows": + osInfo = GetInfoForWindows() + case "darwin": + osInfo = GetInfo("-srm") + case "linux": + osInfo = GetInfo("-srio") + case "freebsd": + osInfo = GetInfo("-sri") + case "openbsd": + osInfo = GetInfo("-srm") + case "netbsd": + osInfo = GetInfo("-srm") + default: + osInfo = OSInfo{ + Name: runtime.GOOS, + Arch: runtime.GOARCH, + } + } + + return map[string]interface{}{ + "type": osInfo.Name, + "details": map[string]interface{}{ + "arch": osInfo.Arch, + "version": osInfo.Version, + }, + } +} + +func GetInfo(flags string) OSInfo { + out, err := _uname(flags) + tries := 0 + for strings.Contains(out, "broken pipe") && tries < 3 { + out, err = _uname(flags) + time.Sleep(500 * time.Millisecond) + tries++ + } + if strings.Contains(out, "broken pipe") || err != nil { + out = "" + } + + if err != nil { + log.Printf("[ERROR] failed to get the OS info: %s", err) + } + core := _retrieveCore(out) + return OSInfo{ + Name: runtime.GOOS, + Arch: runtime.GOARCH, + Version: core, + } +} + +func _uname(flags string) (string, error) { + cmd := exec.Command("uname", flags) + cmd.Stdin = strings.NewReader("some input") + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + err := cmd.Run() + return out.String(), err +} + +func _retrieveCore(osStr string) string { + osStr = strings.Replace(osStr, "\n", "", -1) + osStr = strings.Replace(osStr, "\r\n", "", -1) + osInfo := strings.Split(osStr, " ") + + var core string + if len(osInfo) > 1 { + core = osInfo[1] + } + return core +} + +func GetInfoForWindows() OSInfo { + cmd := exec.Command("cmd", "ver") + cmd.Stdin = strings.NewReader("some input") + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + log.Printf("[ERROR] failed to get the OS info: %s", err) + return OSInfo{ + Name: runtime.GOOS, + Arch: runtime.GOARCH, + } + } + + osStr := strings.Replace(out.String(), "\n", "", -1) + osStr = strings.Replace(osStr, "\r\n", "", -1) + tmp1 := strings.Index(osStr, "[Version") + tmp2 := strings.Index(osStr, "]") + var ver string + if tmp1 == -1 || tmp2 == -1 { + ver = "" + } else { + ver = osStr[tmp1+9 : tmp2] + } + + osInfo := OSInfo{ + Name: runtime.GOOS, + Arch: runtime.GOARCH, + Version: ver, + } + return osInfo +} diff --git a/internal/hcp/registry/metadata.vcs.go b/internal/hcp/registry/metadata.vcs.go new file mode 100644 index 00000000000..53b90316b3e --- /dev/null +++ b/internal/hcp/registry/metadata.vcs.go @@ -0,0 +1,93 @@ +package registry + +import ( + "log" + "os" + + gt "github.com/go-git/go-git/v5" +) + +type VCS interface { + Detect(wd string) error + Details() map[string]interface{} + Type() string +} + +type Git struct { + repo *gt.Repository +} + +func (g *Git) Detect(wd string) error { + repo, err := gt.PlainOpenWithOptions(wd, >.PlainOpenOptions{DetectDotGit: true}) + if err != nil { + return err + } + + g.repo = repo + return nil +} + +func (g *Git) hasUncommittedChanges() bool { + worktree, err := g.repo.Worktree() + if err != nil { + log.Printf("[ERROR] failed to get the git worktree: %s", err) + return false + } + + status, err := worktree.Status() + if err != nil { + log.Printf("[ERROR] failed to get the git worktree status: %s", err) + return false + } + return !status.IsClean() +} + +func (g *Git) Type() string { + return "git" +} + +func (g *Git) Details() map[string]interface{} { + resp := map[string]interface{}{} + + headRef, err := g.repo.Head() + if err != nil { + log.Printf("[ERROR] failed to get the git branch name: %s", err) + } else { + resp["ref"] = headRef.Name().Short() + } + + commit, err := g.repo.CommitObject(headRef.Hash()) + if err != nil { + log.Printf("[ERROR] failed to get the git commit hash: %s", err) + } else { + resp["commit"] = commit.Hash.String() + resp["author"] = commit.Author.Name + " <" + commit.Author.Email + ">" + } + + resp["has_uncommitted_changes"] = g.hasUncommittedChanges() + return resp +} + +func GetVcsMetadata() map[string]interface{} { + wd, err := os.Getwd() + if err != nil { + log.Printf("[ERROR] unable to retrieve current directory: %s", err) + return map[string]interface{}{} + } + + vcsSystems := []VCS{ + &Git{}, + } + + for _, vcs := range vcsSystems { + err := vcs.Detect(wd) + if err == nil { + return map[string]interface{}{ + "type": vcs.Type(), + "details": vcs.Details(), + } + } + } + + return nil +} diff --git a/internal/hcp/registry/types.metadata_store.go b/internal/hcp/registry/types.metadata_store.go index a2ddc1d9353..bb910ee0f07 100644 --- a/internal/hcp/registry/types.metadata_store.go +++ b/internal/hcp/registry/types.metadata_store.go @@ -1,20 +1,12 @@ package registry -import ( - "runtime" -) - // Metadata is the global metadata store, it is attached to a registry implementation // and keeps track of the environmental information. // This then can be sent to HCP Packer, so we can present it to users. type Metadata interface { // Gather is the point where we vacuum all the information // relevant from the environment in order to expose it to HCP Packer. - Gather(args map[string]interface{}) error - // Render is called when the metadata is sent to HCP Packer, - // i.e. when a build finishes, so it gets merged with the rest of the - // information that is build-specific - Render() map[string]interface{} + Gather(args map[string]interface{}) } // MetadataStore is the effective implementation of a global store for metadata @@ -24,31 +16,16 @@ type Metadata interface { // arguments to the build command, and environment-related information. type MetadataStore struct { PackerBuildCommandOptions map[string]interface{} - OperatingSystem map[string]string + OperatingSystem map[string]interface{} + Vcs map[string]interface{} + Cicd map[string]interface{} } -func (ms *MetadataStore) Gather(args map[string]interface{}) error { - // Environment data - ms.gatherOperatingSystemDetails() - - // Build arguments +func (ms *MetadataStore) Gather(args map[string]interface{}) { + ms.OperatingSystem = GetOSMetadata() + ms.Cicd = GetCicdMetadata() + ms.Vcs = GetVcsMetadata() ms.PackerBuildCommandOptions = args - - return nil -} - -func (ms *MetadataStore) gatherOperatingSystemDetails() { - ms.OperatingSystem = map[string]string{ - "os": runtime.GOOS, - "arch": runtime.GOARCH, - } -} - -func (ms *MetadataStore) Render() map[string]interface{} { - return map[string]interface{}{ - "operating_system": ms.OperatingSystem, - "packer_build_command_options": ms.PackerBuildCommandOptions, - } } // NilMetadata is a dummy implementation of a Metadata that does nothing. @@ -57,10 +34,4 @@ func (ms *MetadataStore) Render() map[string]interface{} { // collected or kept in memory in this case. type NilMetadata struct{} -func (ns NilMetadata) Gather(args map[string]interface{}) error { - return nil -} - -func (ns NilMetadata) Render() map[string]interface{} { - return map[string]interface{}{} -} +func (ns NilMetadata) Gather(args map[string]interface{}) {} diff --git a/internal/hcp/registry/types.version.go b/internal/hcp/registry/types.version.go index 91d81749702..0caf6229c11 100644 --- a/internal/hcp/registry/types.version.go +++ b/internal/hcp/registry/types.version.go @@ -179,7 +179,7 @@ func (version *Version) statusSummary(ui sdkpacker.Ui) { // AddMetadataToBuild adds metadata to a build in the HCP Packer registry. func (version *Version) AddMetadataToBuild( - ctx context.Context, buildName string, metadata packer.BuildMetadata, + ctx context.Context, buildName string, buildMetadata packer.BuildMetadata, globalMetadata *MetadataStore, ) error { buildToUpdate, err := version.Build(buildName) if err != nil { @@ -187,10 +187,10 @@ func (version *Version) AddMetadataToBuild( } packerMetadata := make(map[string]interface{}) - packerMetadata["version"] = metadata.PackerVersion + packerMetadata["version"] = buildMetadata.PackerVersion var pluginsMetadata []map[string]interface{} - for _, plugin := range metadata.Plugins { + for _, plugin := range buildMetadata.Plugins { pluginMetadata := map[string]interface{}{ "version": plugin.Description.Version, "name": plugin.Name, @@ -198,7 +198,12 @@ func (version *Version) AddMetadataToBuild( pluginsMetadata = append(pluginsMetadata, pluginMetadata) } packerMetadata["plugins"] = pluginsMetadata + packerMetadata["options"] = globalMetadata.PackerBuildCommandOptions + packerMetadata["os"] = globalMetadata.OperatingSystem buildToUpdate.Metadata.Packer = packerMetadata + buildToUpdate.Metadata.Vcs = globalMetadata.Vcs + buildToUpdate.Metadata.Cicd = globalMetadata.Cicd + return nil }