feat: add pulumi segment

This commit is contained in:
Luca Zecca 2024-01-28 19:17:12 +01:00 committed by Jan De Dobbeleer
parent 0674681fc8
commit 8791965f3f
6 changed files with 528 additions and 0 deletions

View file

@ -202,6 +202,8 @@ const (
PLASTIC SegmentType = "plastic" PLASTIC SegmentType = "plastic"
// Project version // Project version
PROJECT SegmentType = "project" PROJECT SegmentType = "project"
// PULUMI writes the pulumi user, store and stack
PULUMI SegmentType = "pulumi"
// PYTHON writes the virtual env name // PYTHON writes the virtual env name
PYTHON SegmentType = "python" PYTHON SegmentType = "python"
// QUASAR writes the QUASAR version and context // QUASAR writes the QUASAR version and context
@ -322,6 +324,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
PHP: func() SegmentWriter { return &segments.Php{} }, PHP: func() SegmentWriter { return &segments.Php{} },
PLASTIC: func() SegmentWriter { return &segments.Plastic{} }, PLASTIC: func() SegmentWriter { return &segments.Plastic{} },
PROJECT: func() SegmentWriter { return &segments.Project{} }, PROJECT: func() SegmentWriter { return &segments.Project{} },
PULUMI: func() SegmentWriter { return &segments.Pulumi{} },
PYTHON: func() SegmentWriter { return &segments.Python{} }, PYTHON: func() SegmentWriter { return &segments.Python{} },
QUASAR: func() SegmentWriter { return &segments.Quasar{} }, QUASAR: func() SegmentWriter { return &segments.Quasar{} },
R: func() SegmentWriter { return &segments.R{} }, R: func() SegmentWriter { return &segments.R{} },

220
src/segments/pulumi.go Normal file
View file

@ -0,0 +1,220 @@
package segments
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"gopkg.in/yaml.v3"
)
const (
FetchStack properties.Property = "fetch_stack"
FetchAbout properties.Property = "fetch_about"
JSON string = "json"
YAML string = "yaml"
pulumiJSON string = "Pulumi.json"
pulumiYAML string = "Pulumi.yaml"
)
type Pulumi struct {
props properties.Properties
env platform.Environment
Stack string
Name string
workspaceSHA1 string
backend
}
type backend struct {
URL string `json:"url"`
User string `json:"user"`
}
type pulumiFileSpec struct {
Name string `yaml:"name" json:"name"`
}
type pulumiWorkSpaceFileSpec struct {
Stack string `json:"stack"`
}
func (p *Pulumi) Template() string {
return "\U000f0d46 {{ .Stack }}{{if .User }} :: {{ .User }}@{{ end }}{{ if .URL }}{{ .URL }}{{ end }}"
}
func (p *Pulumi) Init(props properties.Properties, env platform.Environment) {
p.props = props
p.env = env
}
func (p *Pulumi) Enabled() bool {
if !p.env.HasCommand("pulumi") {
return false
}
err := p.getProjectName()
if err != nil {
p.env.Error(err)
return false
}
if p.props.GetBool(FetchStack, false) {
p.getPulumiStackName()
}
if p.props.GetBool(FetchAbout, false) {
p.getPulumiAbout()
}
return true
}
func (p *Pulumi) getPulumiStackName() {
if len(p.Name) == 0 || len(p.workspaceSHA1) == 0 {
p.env.Debug("pulumi project name or workspace sha1 is empty")
return
}
stackNameFile := p.Name + "-" + p.workspaceSHA1 + "-" + "workspace.json"
homedir := p.env.Home()
workspaceCacheDir := filepath.Join(homedir, ".pulumi", "workspaces")
if !p.env.HasFolder(workspaceCacheDir) || !p.env.HasFilesInDir(workspaceCacheDir, stackNameFile) {
return
}
workspaceCacheFile := filepath.Join(workspaceCacheDir, stackNameFile)
workspaceCacheFileContent := p.env.FileContent(workspaceCacheFile)
var pulumiWorkspaceSpec pulumiWorkSpaceFileSpec
err := json.Unmarshal([]byte(workspaceCacheFileContent), &pulumiWorkspaceSpec)
if err != nil {
p.env.Error(fmt.Errorf("pulumi workspace file decode error"))
return
}
p.env.DebugF("pulumi stack name: %s", pulumiWorkspaceSpec.Stack)
p.Stack = pulumiWorkspaceSpec.Stack
}
func (p *Pulumi) getProjectName() error {
var kind, fileName string
for _, file := range []string{pulumiYAML, pulumiJSON} {
if p.env.HasFiles(file) {
fileName = file
kind = filepath.Ext(file)[1:]
}
}
if len(kind) == 0 {
return fmt.Errorf("no pulumi spec file found")
}
var pulumiFileSpec pulumiFileSpec
var err error
pulumiFile := p.env.FileContent(fileName)
switch kind {
case YAML:
err = yaml.Unmarshal([]byte(pulumiFile), &pulumiFileSpec)
case JSON:
err = json.Unmarshal([]byte(pulumiFile), &pulumiFileSpec)
default:
err = fmt.Errorf("unknown pulumi spec file format")
}
if err != nil {
p.env.Error(err)
return nil
}
p.Name = pulumiFileSpec.Name
sha1HexString := func(value string) string {
h := sha1.New()
_, err := h.Write([]byte(value))
if err != nil {
p.env.Error(err)
return ""
}
return hex.EncodeToString(h.Sum(nil))
}
p.workspaceSHA1 = sha1HexString(p.env.Pwd() + "/" + fileName)
return nil
}
func (p *Pulumi) getPulumiAbout() {
if len(p.Stack) == 0 {
p.env.Error(fmt.Errorf("pulumi stack name is empty, use `fetch_stack` property to enable stack fetching"))
return
}
cacheKey := "pulumi-" + p.Name + "-" + p.Stack + "-" + p.workspaceSHA1 + "-about"
getAboutCache := func(key string) (*backend, error) {
aboutBackend, OK := p.env.Cache().Get(key)
if (!OK || len(aboutBackend) == 0) || (OK && len(aboutBackend) == 0) {
return nil, fmt.Errorf("no data in cache")
}
var backend *backend
err := json.Unmarshal([]byte(aboutBackend), &backend)
if err != nil {
p.env.DebugF("unable to decode about cache: %s", aboutBackend)
p.env.Error(fmt.Errorf("pulling about cache decode error"))
return nil, err
}
return backend, nil
}
aboutBackend, err := getAboutCache(cacheKey)
if err == nil {
p.backend = *aboutBackend
return
}
aboutOutput, err := p.env.RunCommand("pulumi", "about", "--json")
if err != nil {
p.env.Error(fmt.Errorf("unable to get pulumi about output"))
return
}
var about struct {
Backend *backend `json:"backend"`
}
err = json.Unmarshal([]byte(aboutOutput), &about)
if err != nil {
p.env.Error(fmt.Errorf("pulumi about output decode error"))
return
}
if about.Backend == nil {
p.env.Debug("pulumi about backend is not set")
return
}
p.backend = *about.Backend
cacheTimeout := p.props.GetInt(properties.CacheTimeout, 43800)
jso, _ := json.Marshal(about.Backend)
p.env.Cache().Set(cacheKey, string(jso), cacheTimeout)
}

234
src/segments/pulumi_test.go Normal file
View file

@ -0,0 +1,234 @@
package segments
import (
"errors"
"path/filepath"
"testing"
"github.com/jandedobbeleer/oh-my-posh/src/mock"
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/stretchr/testify/assert"
mock2 "github.com/stretchr/testify/mock"
)
func TestPulumi(t *testing.T) {
cases := []struct {
Case string
YAMLConfig string
JSONConfig string
HasCommand bool
FetchStack bool
Stack string
StackError error
HasWorkspaceFolder bool
WorkSpaceFile string
FetchAbout bool
About string
AboutError error
AboutCache string
ExpectedString string
ExpectedEnabled bool
}{
{
Case: "no pulumi command",
ExpectedEnabled: false,
HasCommand: false,
},
{
Case: "pulumi command is present, but no pulumi file",
ExpectedEnabled: false,
HasCommand: true,
},
{
Case: "pulumi file YAML is present",
ExpectedString: "\U000f0d46",
ExpectedEnabled: true,
HasCommand: true,
YAMLConfig: `
name: oh-my-posh
runtime: golang
description: A Console App
`,
},
{
Case: "pulumi file JSON is present",
ExpectedString: "\U000f0d46",
ExpectedEnabled: true,
HasCommand: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
},
{
Case: "no stack present",
ExpectedString: "\U000f0d46 1337",
ExpectedEnabled: true,
HasCommand: true,
HasWorkspaceFolder: true,
FetchStack: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{ "stack": "1337" }`,
},
{
Case: "pulumi stack",
ExpectedString: "\U000f0d46 1337",
ExpectedEnabled: true,
HasCommand: true,
HasWorkspaceFolder: true,
FetchStack: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{ "stack": "1337" }`,
},
{
Case: "pulumi URL",
ExpectedString: "\U000f0d46 1337 :: posh-user@s3://test-pulumi-state-test",
ExpectedEnabled: true,
HasCommand: true,
HasWorkspaceFolder: true,
FetchStack: true,
FetchAbout: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{ "stack": "1337" }`,
About: `{ "backend": { "url": "s3://test-pulumi-state-test", "user":"posh-user" } }`,
},
{
Case: "pulumi URL - cache",
ExpectedString: "\U000f0d46 1337 :: posh-user@s3://test-pulumi-state-test",
ExpectedEnabled: true,
HasCommand: true,
HasWorkspaceFolder: true,
FetchStack: true,
FetchAbout: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{ "stack": "1337" }`,
AboutCache: `{ "url": "s3://test-pulumi-state-test", "user":"posh-user" }`,
},
// Error flows
{
Case: "pulumi file JSON error",
ExpectedString: "\U000f0d46",
ExpectedEnabled: true,
FetchStack: true,
HasCommand: true,
JSONConfig: `{`,
},
{
Case: "pulumi workspace file JSON error",
ExpectedString: "\U000f0d46",
ExpectedEnabled: true,
FetchStack: true,
HasCommand: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{`,
HasWorkspaceFolder: true,
},
{
Case: "pulumi URL, no fetch_stack set",
ExpectedString: "\U000f0d46",
ExpectedEnabled: true,
HasCommand: true,
FetchAbout: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
},
{
Case: "pulumi URL - cache error",
ExpectedString: "\U000f0d46 1337 :: posh-user@s3://test-pulumi-state-test-output",
ExpectedEnabled: true,
HasCommand: true,
HasWorkspaceFolder: true,
FetchStack: true,
FetchAbout: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{ "stack": "1337" }`,
AboutCache: `{`,
About: `{ "backend": { "url": "s3://test-pulumi-state-test-output", "user":"posh-user" } }`,
},
{
Case: "pulumi URL - about error",
ExpectedString: "\U000f0d46 1337",
ExpectedEnabled: true,
HasCommand: true,
HasWorkspaceFolder: true,
FetchStack: true,
FetchAbout: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{ "stack": "1337" }`,
AboutError: errors.New("error"),
},
{
Case: "pulumi URL - about decode error",
ExpectedString: "\U000f0d46 1337",
ExpectedEnabled: true,
HasCommand: true,
HasWorkspaceFolder: true,
FetchStack: true,
FetchAbout: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{ "stack": "1337" }`,
About: `{`,
},
{
Case: "pulumi URL - about backend is nil",
ExpectedString: "\U000f0d46 1337",
ExpectedEnabled: true,
HasCommand: true,
HasWorkspaceFolder: true,
FetchStack: true,
FetchAbout: true,
JSONConfig: `{ "name": "oh-my-posh" }`,
WorkSpaceFile: `{ "stack": "1337" }`,
About: `{}`,
},
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("HasCommand", "pulumi").Return(tc.HasCommand)
env.On("RunCommand", "pulumi", []string{"stack", "ls", "--json"}).Return(tc.Stack, tc.StackError)
env.On("RunCommand", "pulumi", []string{"about", "--json"}).Return(tc.About, tc.AboutError)
env.On("Pwd").Return("/home/foobar/Work/oh-my-posh/pulumi/projects/awesome-project")
env.On("Home").Return(filepath.Clean("/home/foobar"))
env.On("Error", mock2.Anything)
env.On("Debug", mock2.Anything)
env.On("DebugF", mock2.Anything, mock2.Anything)
env.On("HasFiles", pulumiYAML).Return(len(tc.YAMLConfig) > 0)
env.On("FileContent", pulumiYAML).Return(tc.YAMLConfig, nil)
env.On("HasFiles", pulumiJSON).Return(len(tc.JSONConfig) > 0)
env.On("FileContent", pulumiJSON).Return(tc.JSONConfig, nil)
env.On("HasFolder", filepath.Clean("/home/foobar/.pulumi/workspaces")).Return(tc.HasWorkspaceFolder)
workspaceFile := "oh-my-posh-c62b7b6786c5c5a85896576e46a25d7c9f888e92-workspace.json"
env.On("HasFilesInDir", filepath.Clean("/home/foobar/.pulumi/workspaces"), workspaceFile).Return(len(tc.WorkSpaceFile) > 0)
env.On("FileContent", filepath.Clean("/home/foobar/.pulumi/workspaces/"+workspaceFile)).Return(tc.WorkSpaceFile, nil)
cache := &mock.MockedCache{}
cache.On("Get", "pulumi-oh-my-posh-1337-c62b7b6786c5c5a85896576e46a25d7c9f888e92-about").Return(tc.AboutCache, len(tc.AboutCache) > 0)
cache.On("Set", mock2.Anything, mock2.Anything, mock2.Anything)
env.On("Cache").Return(cache)
pulumi := &Pulumi{
env: env,
props: properties.Map{
FetchStack: tc.FetchStack,
FetchAbout: tc.FetchAbout,
},
}
assert.Equal(t, tc.ExpectedEnabled, pulumi.Enabled(), tc.Case)
if !tc.ExpectedEnabled {
continue
}
var got = renderTemplate(env, pulumi.Template(), pulumi)
assert.Equal(t, tc.ExpectedString, got, tc.Case)
}
}

View file

@ -295,6 +295,7 @@
"php", "php",
"plastic", "plastic",
"project", "project",
"pulumi",
"root", "root",
"ruby", "ruby",
"rust", "rust",
@ -1957,6 +1958,19 @@
} }
} }
}, },
{
"if": {
"properties": {
"type": {
"const": "pulumi"
}
}
},
"then": {
"title": "Pulumi Segment",
"description": "https://ohmyposh.dev/docs/segments/pulumi"
}
},
{ {
"if": { "if": {
"properties": { "properties": {

View file

@ -0,0 +1,56 @@
---
id: pulumi
title: Pulumi
sidebar_label: Pulumi
---
## What
Display the currently active pulumi logged-in user, url and stack.
:::caution
This requires a pulumi binary in your PATH and will only show in directories that contain a `Pulumi.yaml` file.
:::
## Sample Configuration
import Config from "@site/src/components/Config.js";
<Config
data={{
type: "pulumi",
style: "diamond",
powerline_symbol: "\uE0CF",
foreground: "#ffffff",
background: "#662d91",
template:
"{{ .Stack }}{{if .User }} :: {{ .User }}@{{ end }}{{ if .URL }}{{ .URL }}{{ end }}",
}}
/>
## Properties
| Name | Type | Default | Description |
| ------------- | --------- | ------- | ---------------------------------------------------------------------------------- |
| `fetch_stack` | `boolean` | `false` | fetch the current stack name |
| `fetch_about` | `boolean` | `false` | fetch the URL and user for the current stask. Requires `fetch_stack` set to `true` |
## Template ([info][templates])
:::note default template
```template
{{ .Stack }}{{if .User }} :: {{ .User }}@{{ end }}{{ if .URL }}{{ .URL }}{{ end }}
```
:::
### Properties
| Name | Type | Description |
| -------- | -------- | -------------------------------------------------- |
| `.Stack` | `string` | the current stack name |
| `.User` | `string` | is the current logged in user |
| `.Url` | `string` | the URL of the state where pulumi stores resources |
[templates]: /docs/configuration/templates

View file

@ -104,6 +104,7 @@ module.exports = {
"segments/php", "segments/php",
"segments/plastic", "segments/plastic",
"segments/project", "segments/project",
"segments/pulumi",
"segments/python", "segments/python",
"segments/quasar", "segments/quasar",
"segments/r", "segments/r",