diff --git a/README.md b/README.md index c448ef0..6b447d3 100644 --- a/README.md +++ b/README.md @@ -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: * `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 outputs an item, it should write the item's JSON representation to `stdout` on one line. diff --git a/cmd/sourceEdit.go b/cmd/sourceEdit.go deleted file mode 100644 index 8b80ae0..0000000 --- a/cmd/sourceEdit.go +++ /dev/null @@ -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) -} diff --git a/cmd/sourceEnv.go b/cmd/sourceEnv.go new file mode 100644 index 0000000..cddeca3 --- /dev/null +++ b/cmd/sourceEnv.go @@ -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) + } +} diff --git a/core/env.go b/core/env.go new file mode 100644 index 0000000..6868f57 --- /dev/null +++ b/core/env.go @@ -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 + }) +} diff --git a/core/env_test.go b/core/env_test.go new file mode 100644 index 0000000..01bd7cd --- /dev/null +++ b/core/env_test.go @@ -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]) + } +} diff --git a/core/sql/0001_initial_schema.sql b/core/sql/0001_initial_schema.sql index 7c3fd67..1c3dab5 100644 --- a/core/sql/0001_initial_schema.sql +++ b/core/sql/0001_initial_schema.sql @@ -10,6 +10,13 @@ create table actions( primary key (source, name), foreign key (source) references sources (name) on delete cascade ) 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( source text not null, id text not null,