Add source environment variables

This commit is contained in:
Tim Van Baak 2025-01-31 07:50:56 -08:00
parent 60afdd2a32
commit a238d1f239
6 changed files with 210 additions and 22 deletions

View File

@ -113,7 +113,8 @@ To execute an action, Intake executes the command specified by that action's `ar
The process's environment is as follows: The process's environment is as follows:
* `intake`'s environment is inherited. * `intake`'s environment is inherited.
* `STATE_PATH` is set to the absolute path of a file containing the source's persistent state. * Each environment variable defined in the source is set.
* `STATE_PATH` is set to the absolute path of a file that the source can use for persistent state. This file can be used for any data in any format. Changes to the state file are only saved if the action succeeds.
When an action receives an item as input, that item's JSON representation is written to that action's `stdin`. When an action receives an item as input, that item's JSON representation is written to that action's `stdin`.
When an action outputs an item, it should write the item's JSON representation to `stdout` on one line. When an action outputs an item, it should write the item's JSON representation to `stdout` on one line.

View File

@ -1,21 +0,0 @@
package cmd
import (
"log"
"github.com/spf13/cobra"
)
var sourceEditCmd = &cobra.Command{
Use: "edit",
Short: "Edit a source",
Long: `
`,
Run: func(cmd *cobra.Command, args []string) {
log.Fatal("not implemented")
},
}
func init() {
sourceCmd.AddCommand(sourceEditCmd)
}

53
cmd/sourceEnv.go Normal file
View File

@ -0,0 +1,53 @@
package cmd
import (
"fmt"
"log"
"github.com/Jaculabilis/intake/core"
"github.com/spf13/cobra"
)
var sourceEnvCmd = &cobra.Command{
Use: "env",
Short: "Manage source environment variables",
Long: `Add, edit, list, or delete environment variables.
When --set is not specified, list the environment for the source.
--set KEY=VALUE will add or edit an environment variable to be set in all
action executions.
--set KEY= will delete the environment variable from the source.
`,
Run: func(cmd *cobra.Command, args []string) {
sourceEnv(stringArg(cmd, "source"), stringArrayArg(cmd, "set"))
},
}
func init() {
sourceCmd.AddCommand(sourceEnvCmd)
sourceEnvCmd.Flags().StringP("source", "s", "", "Source to edit")
sourceEnvCmd.MarkFlagRequired("source")
sourceEnvCmd.Flags().StringArray("set", nil, "Set or modify environment variable")
}
func sourceEnv(source string, env []string) {
db := openAndMigrateDb()
if len(env) == 0 {
envs, err := core.GetEnvs(db, source)
if err != nil {
log.Fatalf("failed to get envs: %v", err)
}
for _, env := range envs {
fmt.Println(env)
}
}
if err := core.SetEnvs(db, source, env); err != nil {
log.Fatalf("failed to set envs: %v", err)
}
}

57
core/env.go Normal file
View File

@ -0,0 +1,57 @@
package core
import (
"database/sql"
"fmt"
"strings"
)
func GetEnvs(db *DB, source string) ([]string, error) {
rows, err := db.Query(`
select name, value
from envs
where source = ?
`, source)
if err != nil {
return nil, err
}
var envs []string
for rows.Next() {
var name string
var value string
if err := rows.Scan(&name, &value); err != nil {
return nil, err
}
envs = append(envs, fmt.Sprintf("%s=%s", name, value))
}
return envs, nil
}
func SetEnvs(db *DB, source string, envs []string) error {
return db.Transact(func(tx *sql.Tx) error {
for _, env := range envs {
parts := strings.SplitN(env, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid env format: %s", env)
}
if parts[1] == "" {
_, err := tx.Exec(`
delete from envs
where source = ? and name = ?
`, source, parts[0])
if err != nil {
return fmt.Errorf("failed to clear source %s env %s: %v", source, parts[0], err)
}
} else {
_, err := tx.Exec(`
insert into envs (source, name, value)
values (?, ?, ?)
`, source, parts[0], parts[1])
if err != nil {
return fmt.Errorf("failed to set source %s env %s = %s: %v", source, parts[0], parts[1], err)
}
}
}
return nil
})
}

91
core/env_test.go Normal file
View File

@ -0,0 +1,91 @@
package core
import (
"slices"
"testing"
)
func TestEnvs(t *testing.T) {
db := EphemeralDb(t)
if err := AddSource(db, "_"); err != nil {
t.Fatal(err)
}
// Insert env
if err := SetEnvs(db, "_", []string{"ONE=hello"}); err != nil {
t.Fatal(err)
}
envs, err := GetEnvs(db, "_")
if err != nil {
t.Fatal(err)
}
if len(envs) != 1 {
t.Fatal("expected 1 env")
}
if envs[0] != "ONE=hello" {
t.Fatalf("Expected ONE=hello, got %s", envs[0])
}
// Insert env with = in value
if err := SetEnvs(db, "_", []string{"TWO=world=true"}); err != nil {
t.Fatal(err)
}
envs, err = GetEnvs(db, "_")
if err != nil {
t.Fatal(err)
}
if len(envs) != 2 {
t.Fatal("expected 2 envs")
}
slices.Sort(envs) // ONE > TWO
if envs[1] != "TWO=world=true" {
t.Fatalf("Expected TWO=world=true, got %s", envs[1])
}
// Replace env
if err := SetEnvs(db, "_", []string{"TWO=goodbye"}); err != nil {
t.Fatal(err)
}
envs, err = GetEnvs(db, "_")
if err != nil {
t.Fatal(err)
}
if len(envs) != 2 {
t.Fatal("expected 2 envs")
}
slices.Sort(envs) // ONE > TWO
if envs[1] != "TWO=goodbye" {
t.Fatalf("Expected TWO=goodbye, got %s", envs[1])
}
// Insert is transactional on error
if err := SetEnvs(db, "_", []string{"THREE=crowd", "FOUR"}); err == nil {
t.Fatal("expected bad env insert to fail")
}
envs, err = GetEnvs(db, "_")
if err != nil {
t.Fatal(err)
}
if len(envs) != 2 {
t.Fatal("expected 2 envs after failed insert")
}
slices.Sort(envs) // ONE > TWO
if envs[0] != "ONE=hello" || envs[1] != "TWO=goodbye" {
t.Fatalf("Expected ONE=hello and TWO=goodbye, got %v", envs)
}
// Delete env
if err := SetEnvs(db, "_", []string{"ONE="}); err != nil {
t.Fatal(err)
}
envs, err = GetEnvs(db, "_")
if err != nil {
t.Fatal(err)
}
if len(envs) != 1 {
t.Fatal("expected 1 env after deletion")
}
if envs[0] != "TWO=goodbye" {
t.Fatalf("Expected TWO=goodbye, got %s", envs[0])
}
}

View File

@ -10,6 +10,13 @@ create table actions(
primary key (source, name), primary key (source, name),
foreign key (source) references sources (name) on delete cascade foreign key (source) references sources (name) on delete cascade
) strict; ) strict;
create table envs(
source text not null,
name text not null,
value text not null,
unique (source, name) on conflict replace,
foreign key (source) references sources (name) on delete cascade
) strict;
create table items( create table items(
source text not null, source text not null,
id text not null, id text not null,