Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions public/tour-pipelines/Secrets.pipeline.component.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: Secrets
description: |
A tiny authenticated pipeline used by the "Using Secrets" guided tour. The
Authenticated Request task reads a bearer token and an API key, sends them as
headers to https://httpbin.org/headers, and prints a masked response. Neither
credential is ever written into the pipeline definition: assign a secret to the
task's `token` argument with the lightning-bolt menu, and bind a secret to the
`API_KEY` input when you submit.
metadata:
annotations:
editor.flow-direction: left-to-right
inputs:
- name: API_KEY
description: |
API key for the request. Left empty here so you can bind a secret to it at
submit time via run arguments.
optional: true
annotations:
editor.position: '{"x": -180, "y": 120}'
outputs:
- name: HTTP Response
annotations:
editor.position: '{"x": 920, "y": 80}'
implementation:
graph:
tasks:
Authenticated Request:
componentRef:
name: Authenticated Request
spec:
name: Authenticated Request
description: |
Reads a bearer token and an API key (each injected from a Tangle
secret as a file), sends them as request headers to
https://httpbin.org/headers, masks the sensitive headers, and writes
the response body to its output. Credential values are never logged.
inputs:
- name: token
description: Bearer token, injected from a Tangle secret.
optional: true
- name: api_key
description: API key, injected from a Tangle secret.
optional: true
outputs:
- name: response
type: String
description: httpbin's JSON response body, with secrets masked.
implementation:
container:
image: python:3.12-slim
command:
- python3
- '-u'
- '-c'
- |
import argparse, json, os, urllib.request

parser = argparse.ArgumentParser()
parser.add_argument("--token")
parser.add_argument("--api-key", dest="api_key")
parser.add_argument("--output", required=True)
args = parser.parse_args()

headers = {}
if args.token:
with open(args.token) as f:
token = f.read().strip()
headers["Authorization"] = f"Bearer {token}"
print(f"loaded token: length={len(token)} (value not logged)")
if args.api_key:
with open(args.api_key) as f:
api_key = f.read().strip()
headers["X-Api-Key"] = api_key
print(f"loaded api_key: length={len(api_key)} (value not logged)")

req = urllib.request.Request(
"https://httpbin.org/headers", headers=headers
)
with urllib.request.urlopen(req, timeout=30) as resp:
print(f"http status: {resp.status}")
body = resp.read().decode("utf-8")

parsed = json.loads(body)
# httpbin echoes the request headers back, so mask the sensitive
# ones before writing them to the pipeline output.
echoed = parsed.get("headers", {})
if "Authorization" in echoed:
echoed["Authorization"] = "Bearer <redacted>"
if "X-Api-Key" in echoed:
echoed["X-Api-Key"] = "<redacted>"
masked = json.dumps(parsed, indent=2)
print(masked)

os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w") as f:
f.write(masked + "\n")
args:
- if:
cond:
isPresent: token
then:
- '--token'
- inputPath: token
- if:
cond:
isPresent: api_key
then:
- '--api-key'
- inputPath: api_key
- '--output'
- outputPath: response
arguments:
api_key:
graphInput:
inputName: API_KEY
annotations:
editor.position: '{"x": 160, "y": 80}'
cloud-pipelines.net/launchers/generic/resources.cpu: '1'
cloud-pipelines.net/launchers/generic/resources.memory: 512Mi
Show Response:
componentRef:
name: Show Response
spec:
name: Show Response
description: Echoes the masked HTTP response to stdout and to the pipeline output.
inputs:
- name: response
type: String
outputs:
- name: echoed
type: String
implementation:
container:
image: python:3.12-slim
command:
- sh
- '-ec'
- |
cat "$0"
mkdir -p "$(dirname "$1")"
cp "$0" "$1"
- inputPath: response
- outputPath: echoed
arguments:
response:
taskOutput:
outputName: response
taskId: Authenticated Request
annotations:
editor.position: '{"x": 560, "y": 80}'
outputValues:
HTTP Response:
taskOutput:
outputName: echoed
taskId: Show Response
140 changes: 140 additions & 0 deletions src/components/Learn/tours/usingSecrets.tour.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
{
"id": "using-secrets",
"displayName": "Guided Tour: Using Secrets",
"requiresEditor": true,
"starterPipelineUrl": "tour-pipelines/Secrets.pipeline.component.yaml",
"mockBackend": true,
"steps": [
{
"selector": "[data-tour-anchor=\"no-spotlight\"]",
"content": "Let's talk about **Secrets**.\n\nEverything you put in a pipeline (argument values, input defaults, run settings) is saved with it and visible to anyone who can access your Tangle instance. If they can open this pipeline or one of its runs, they can read those values.\n\nSo typing an API key or token straight into an argument quietly shares it with everyone, and writes it into the pipeline YAML too.",
"position": "center"
},
{
"selector": "[data-tour-anchor=\"no-spotlight\"]",
"content": "That's where **secrets** come in.\n\nA secret keeps its value on the backend under a name you choose. Tangle only ever stores and sends the **name**. The value gets injected into the running container when the pipeline runs, and it never shows up anywhere in the app.",
"position": "center"
},
{
"selector": "[data-tour-card=\"task\"][data-tour-card-name=\"Authenticated Request\"]",
"highlightedSelectors": [
"[data-tour-card=\"task\"][data-tour-card-name=\"Authenticated Request\"]"
],
"resizeObservables": ["[data-tour=\"editor-canvas\"]"],
"content": "Let's create a secret and put it to use right in the **argument editor**.\n\nStart by selecting the **Authenticated Request** task. Its inputs show up in the **Task Properties** panel on the right.",
"position": "top",
"stepInteraction": true,
"interaction": "select-task",
"targetTaskName": "Authenticated Request"
},
{
"selector": "[data-dock-window-content=\"context-panel\"]",
"highlightedSelectors": [
"[data-dock-window-content=\"context-panel\"]",
"[data-tour=\"thunder-menu-content\"]",
"[data-tour=\"thunder-menu-submenu-content\"]"
],
"mutationObservables": [
"[data-dock-window-content=\"context-panel\"]",
"[data-tour=\"thunder-menu-content\"]",
"[data-tour=\"thunder-menu-submenu-content\"]"
],
"resizeObservables": ["[data-dock-window-content=\"context-panel\"]"],
"requiresTaskSelected": "Authenticated Request",
"content": "Now open the secret picker for the `token` argument.\n\nHover its row to reveal the **⚡** button, click it, then choose **Dynamic Data → Select Secret**.",
"position": "left",
"stepInteraction": true,
"interaction": "open-secret-dialog"
},
{
"selector": "[data-testid=\"select-secret-dialog\"]",
"highlightedSelectors": ["[data-testid=\"select-secret-dialog\"]"],
"mutationObservables": ["[data-testid=\"select-secret-dialog\"]"],
"resizeObservables": ["[data-testid=\"select-secret-dialog\"]"],
"content": "Click **Add Secret** and give it a name and a value. The value is write only, so Tangle won't ever show it back to you. Save it, then pick it from the list to attach it to the argument.\n\nOnly the secret's **name** ends up in the pipeline.",
"position": "left",
"stepInteraction": true,
"interaction": "assign-secret-argument",
"targetArgumentName": "token"
},
{
"selector": "[data-dock-window-content=\"context-panel\"]",
"highlightedSelectors": ["[data-dock-window-content=\"context-panel\"]"],
"mutationObservables": ["[data-dock-window-content=\"context-panel\"]"],
"resizeObservables": ["[data-dock-window-content=\"context-panel\"]"],
"content": "Nice. The `token` argument now shows your secret **by name**, with a little lock icon, instead of its value.\n\nThe value stays on the backend and the pipeline only points at the name. Anyone you share this with sees the reference, never the credential.",
"position": "left"
},
{
"selector": "[data-tracking-id=\"v2.header.settings\"]",
"highlightedSelectors": ["[data-tour=\"editor-top-bar-actions\"]"],
"ringSelectors": ["[data-tracking-id=\"v2.header.settings\"]"],
"tourPanel": "secrets-manager",
"content": "Tangle has a dedicated **secrets manager**.\n\nOpen it by clicking the **Settings** gear in the top right.",
"position": "bottom",
"stepInteraction": true,
"interaction": "open-settings-panel"
},
{
"selector": "[data-tour=\"tour-settings-dialog\"]",
"highlightedSelectors": ["[data-tour=\"tour-settings-dialog\"]"],
"mutationObservables": ["[data-tour=\"tour-settings-dialog\"]"],
"resizeObservables": ["[data-tour=\"tour-settings-dialog\"]"],
"tourPanel": "secrets-manager",
"content": "This is **Settings → Secrets**, where all your secrets live. They're listed by name only, never by value.\n\nYou can **replace** a secret's value here (the change reaches every pipeline that uses it, and the value still never appears) or **delete** one you no longer need. Take a look, then carry on."
},
{
"selector": "[data-tour-card=\"input\"][data-tour-card-name=\"API_KEY\"]",
"highlightedSelectors": [
"[data-tour-card=\"input\"][data-tour-card-name=\"API_KEY\"]"
],
"resizeObservables": ["[data-tour=\"editor-canvas\"]"],
"content": "There's a second way to hand a secret to a pipeline, and it keeps the pipeline itself generic.\n\nInstead of baking a specific secret into a task, you can leave a pipeline **input** empty and choose a secret for it **when you submit a run**. The pipeline definition then mentions no secret at all, and everyone can run it with their own.\n\nThis pipeline's **API_KEY** input was left blank for exactly that.",
"position": "top"
},
{
"selector": "[data-dock-window-content=\"runs-and-submission\"]",
"highlightedSelectors": [
"[data-dock-window-content=\"runs-and-submission\"]"
],
"ringSelectors": ["[data-testid=\"run-with-arguments-button\"]"],
"mutationObservables": [
"[data-dock-window-content=\"runs-and-submission\"]"
],
"resizeObservables": [
"[data-dock-window-content=\"runs-and-submission\"]"
],
"ensureWindowRestored": "runs-and-submission",
"content": "In **Runs and submission**, click the **split-arrows** icon next to Submit to open the run arguments dialog.",
"position": "right",
"stepInteraction": true,
"interaction": "open-submit-dialog"
},
{
"selector": "[data-tour=\"submit-arguments-dialog\"]",
"highlightedSelectors": [
"[data-tour=\"submit-arguments-dialog\"]",
"[data-testid=\"select-secret-dialog\"]"
],
"mutationObservables": [
"[data-tour=\"submit-arguments-dialog\"]",
"[data-testid=\"select-secret-dialog\"]"
],
"resizeObservables": ["[data-tour=\"submit-arguments-dialog\"]"],
"content": "Find the **API_KEY** input, hover it, and click the **lock** (\"Use Secret\") button. Pick your secret to set it for this run. The value is supplied only at submit time and never saved into the pipeline.",
"position": "left",
"stepInteraction": true,
"interaction": "assign-secret-submit",
"targetArgumentName": "API_KEY"
},
{
"selector": "[data-tour=\"submit-arguments-dialog\"]",
"highlightedSelectors": ["[data-tour=\"submit-arguments-dialog\"]"],
"ringSelectors": ["[data-tour=\"submit-arguments-confirm\"]"],
"mutationObservables": ["[data-tour=\"submit-arguments-dialog\"]"],
"resizeObservables": ["[data-tour=\"submit-arguments-dialog\"]"],
"content": "Done! The **API_KEY** input is now backed by your secret. At submit time the value is supplied to the run and never recorded in the pipeline definition.\n\nWith a backend connected you can **Submit Run** to launch it with your secret. Either way, wrap up the tour below.",
"position": "left"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const SubmitTaskArgumentsDialog = ({
<Dialog open={open} onOpenChange={handleCancel} modal={!tourMode}>
<DialogContent
className="sm:max-w-lg"
data-tour="submit-arguments-dialog"
{...(tourMode
? {
onInteractOutside: (event) => event.preventDefault(),
Expand Down Expand Up @@ -216,6 +217,7 @@ export const SubmitTaskArgumentsDialog = ({
<Button
onClick={handleConfirm}
disabled={!isValidToSubmit || mockBackend}
data-tour="submit-arguments-confirm"
>
Submit Run
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export const ThunderMenu = observer(function ThunderMenu({
side="bottom"
sideOffset={4}
className="w-56 z-[9999]"
data-tour="thunder-menu-content"
>
<DropdownMenuItem
disabled={!canReset}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export function DynamicDataSubmenu({
<Icon name="Zap" size="sm" className="text-purple-600" />
Dynamic Data
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-52 z-[9999]">
<DropdownMenuSubContent
className="w-52 z-[9999]"
data-tour="thunder-menu-submenu-content"
>
{groups.map((group, index) => (
<DropdownMenuGroup key={group.id}>
{index > 0 && <DropdownMenuSeparator />}
Expand Down
Loading