pipekit-sdk 2.0.1__tar.gz → 2.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/PKG-INFO +47 -5
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/README.md +39 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/pyproject.toml +4 -3
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/v1.py +53 -73
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/v1alpha1.py +9 -9
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/service.py +269 -28
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/LICENSE +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/__init__.py +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/_helpers.py +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/__init__.py +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/_config.py +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/big.py +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/inf.py +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/intstr.py +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/model.py +0 -0
- {pipekit_sdk-2.0.1 → pipekit_sdk-2.1.2}/src/pipekit_sdk/models/resource.py +0 -0
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pipekit-sdk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Pipekit Python SDK
|
|
5
|
-
Home-page: https://pipekit.io
|
|
6
5
|
License: BSD-3-Clause
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Keywords: pipekit,hera,argo,workflows,orchestration
|
|
8
8
|
Author: Pipekit
|
|
9
9
|
Author-email: ci@pipekit.io
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.9,<4
|
|
11
11
|
Classifier: License :: OSI Approved :: BSD License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
20
|
Requires-Dist: hera (>=5.5.2)
|
|
20
21
|
Requires-Dist: pydantic (<3)
|
|
22
|
+
Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
|
|
21
23
|
Requires-Dist: responses (>=0.25.3,<0.26.0)
|
|
22
24
|
Project-URL: Documentation, https://docs.pipekit.io/
|
|
25
|
+
Project-URL: Homepage, https://pipekit.io
|
|
23
26
|
Description-Content-Type: text/markdown
|
|
24
27
|
|
|
25
28
|
[](https://pipekit.io)
|
|
@@ -94,6 +97,45 @@ pipekit.submit(w, "<cluster-name>")
|
|
|
94
97
|
pipekit.print_logs(pipe_run.uuid)
|
|
95
98
|
```
|
|
96
99
|
|
|
100
|
+
## Managing CronWorkflows
|
|
101
|
+
|
|
102
|
+
You can create, update, and delete a `CronWorkflow` from Python. The namespace
|
|
103
|
+
must match the one in the manifest on every call (the platform default is
|
|
104
|
+
`argo`). A wrong namespace makes the cron look missing.
|
|
105
|
+
|
|
106
|
+
```py
|
|
107
|
+
from hera.workflows import Container, CronWorkflow
|
|
108
|
+
from pipekit_sdk.service import PipekitService
|
|
109
|
+
|
|
110
|
+
pipekit = PipekitService(token="<token>")
|
|
111
|
+
|
|
112
|
+
with CronWorkflow(
|
|
113
|
+
name="daily-demand-forecast",
|
|
114
|
+
namespace="argo",
|
|
115
|
+
entrypoint="main",
|
|
116
|
+
# Argo Workflows 3.6 deprecated the singular spec.schedule. Use schedules.
|
|
117
|
+
schedules=["*/5 * * * *"],
|
|
118
|
+
service_account_name="argo",
|
|
119
|
+
) as cron:
|
|
120
|
+
Container(name="main", image="alpine", command=["sh", "-c", "echo hello"])
|
|
121
|
+
|
|
122
|
+
# Create
|
|
123
|
+
pipekit.create(cron, "<cluster-name>")
|
|
124
|
+
|
|
125
|
+
# Update: the namespace is taken from the manifest when not passed
|
|
126
|
+
updated = pipekit.update_cron(cron, "<cluster-name>")
|
|
127
|
+
|
|
128
|
+
# Suspend / resume scheduling
|
|
129
|
+
pipekit.suspend_cron("<cluster-name>", "argo", "daily-demand-forecast")
|
|
130
|
+
pipekit.resume_cron("<cluster-name>", "argo", "daily-demand-forecast")
|
|
131
|
+
|
|
132
|
+
# Get the current state
|
|
133
|
+
current = pipekit.get_cron("<cluster-name>", "argo", "daily-demand-forecast")
|
|
134
|
+
|
|
135
|
+
# Delete
|
|
136
|
+
pipekit.delete_cron("<cluster-name>", "argo", "daily-demand-forecast")
|
|
137
|
+
```
|
|
138
|
+
|
|
97
139
|
## Further help
|
|
98
140
|
Please refer to the [Pipekit Documentation](https://docs.pipekit.io) for more information.
|
|
99
141
|
|
|
@@ -70,5 +70,44 @@ pipekit.submit(w, "<cluster-name>")
|
|
|
70
70
|
pipekit.print_logs(pipe_run.uuid)
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
## Managing CronWorkflows
|
|
74
|
+
|
|
75
|
+
You can create, update, and delete a `CronWorkflow` from Python. The namespace
|
|
76
|
+
must match the one in the manifest on every call (the platform default is
|
|
77
|
+
`argo`). A wrong namespace makes the cron look missing.
|
|
78
|
+
|
|
79
|
+
```py
|
|
80
|
+
from hera.workflows import Container, CronWorkflow
|
|
81
|
+
from pipekit_sdk.service import PipekitService
|
|
82
|
+
|
|
83
|
+
pipekit = PipekitService(token="<token>")
|
|
84
|
+
|
|
85
|
+
with CronWorkflow(
|
|
86
|
+
name="daily-demand-forecast",
|
|
87
|
+
namespace="argo",
|
|
88
|
+
entrypoint="main",
|
|
89
|
+
# Argo Workflows 3.6 deprecated the singular spec.schedule. Use schedules.
|
|
90
|
+
schedules=["*/5 * * * *"],
|
|
91
|
+
service_account_name="argo",
|
|
92
|
+
) as cron:
|
|
93
|
+
Container(name="main", image="alpine", command=["sh", "-c", "echo hello"])
|
|
94
|
+
|
|
95
|
+
# Create
|
|
96
|
+
pipekit.create(cron, "<cluster-name>")
|
|
97
|
+
|
|
98
|
+
# Update: the namespace is taken from the manifest when not passed
|
|
99
|
+
updated = pipekit.update_cron(cron, "<cluster-name>")
|
|
100
|
+
|
|
101
|
+
# Suspend / resume scheduling
|
|
102
|
+
pipekit.suspend_cron("<cluster-name>", "argo", "daily-demand-forecast")
|
|
103
|
+
pipekit.resume_cron("<cluster-name>", "argo", "daily-demand-forecast")
|
|
104
|
+
|
|
105
|
+
# Get the current state
|
|
106
|
+
current = pipekit.get_cron("<cluster-name>", "argo", "daily-demand-forecast")
|
|
107
|
+
|
|
108
|
+
# Delete
|
|
109
|
+
pipekit.delete_cron("<cluster-name>", "argo", "daily-demand-forecast")
|
|
110
|
+
```
|
|
111
|
+
|
|
73
112
|
## Further help
|
|
74
113
|
Please refer to the [Pipekit Documentation](https://docs.pipekit.io) for more information.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pipekit-sdk"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.1.2"
|
|
4
4
|
description = "Pipekit Python SDK"
|
|
5
5
|
authors = ["Pipekit <ci@pipekit.io>"]
|
|
6
6
|
license = "BSD-3-Clause"
|
|
@@ -23,10 +23,11 @@ packages = [
|
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
[tool.poetry.dependencies]
|
|
26
|
-
python = ">=3.
|
|
26
|
+
python = ">=3.9,<4"
|
|
27
27
|
hera = ">=5.5.2"
|
|
28
28
|
pydantic = "<3"
|
|
29
29
|
responses = "^0.25.3"
|
|
30
|
+
pyjwt = "^2.9.0"
|
|
30
31
|
|
|
31
32
|
[tool.poetry.group.dev.dependencies]
|
|
32
33
|
ruff = "*"
|
|
@@ -34,7 +35,7 @@ mypy = "^1.11.0"
|
|
|
34
35
|
pytest = "^8.3.1"
|
|
35
36
|
hera = {extras = ["cli"], version = "^5.16.1"}
|
|
36
37
|
types-requests = "^2.32.0.20240712"
|
|
37
|
-
datamodel-code-generator = {extras = ["http"], version = "^0.
|
|
38
|
+
datamodel-code-generator = {extras = ["http"], version = "^0.35.0"}
|
|
38
39
|
|
|
39
40
|
[build-system]
|
|
40
41
|
requires = ["poetry-core"]
|
|
@@ -132,7 +132,7 @@ class AzureFileVolumeSource(BaseModel):
|
|
|
132
132
|
str,
|
|
133
133
|
Field(
|
|
134
134
|
alias="secretName",
|
|
135
|
-
description=("secretName is the name of secret that contains Azure Storage Account
|
|
135
|
+
description=("secretName is the name of secret that contains Azure Storage Account Name and Key"),
|
|
136
136
|
),
|
|
137
137
|
]
|
|
138
138
|
share_name: Annotated[str, Field(alias="shareName", description="shareName is the azure share Name")]
|
|
@@ -182,7 +182,7 @@ class ContainerPort(BaseModel):
|
|
|
182
182
|
Field(
|
|
183
183
|
alias="containerPort",
|
|
184
184
|
description=(
|
|
185
|
-
"Number of port to expose on the pod's IP address. This must be a valid
|
|
185
|
+
"Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536."
|
|
186
186
|
),
|
|
187
187
|
),
|
|
188
188
|
]
|
|
@@ -312,7 +312,7 @@ class FlockerVolumeSource(BaseModel):
|
|
|
312
312
|
Optional[str],
|
|
313
313
|
Field(
|
|
314
314
|
alias="datasetUUID",
|
|
315
|
-
description=("datasetUUID is the UUID of the dataset. This is unique identifier of a
|
|
315
|
+
description=("datasetUUID is the UUID of the dataset. This is unique identifier of a Flocker dataset"),
|
|
316
316
|
),
|
|
317
317
|
] = None
|
|
318
318
|
|
|
@@ -371,7 +371,7 @@ class GCEPersistentDiskVolumeSource(BaseModel):
|
|
|
371
371
|
class GRPCAction(BaseModel):
|
|
372
372
|
port: Annotated[
|
|
373
373
|
int,
|
|
374
|
-
Field(description=("Port number of the gRPC service. Number must be in the range 1 to
|
|
374
|
+
Field(description=("Port number of the gRPC service. Number must be in the range 1 to 65535.")),
|
|
375
375
|
]
|
|
376
376
|
service: Annotated[
|
|
377
377
|
str,
|
|
@@ -613,7 +613,7 @@ class ObjectFieldSelector(BaseModel):
|
|
|
613
613
|
Optional[str],
|
|
614
614
|
Field(
|
|
615
615
|
alias="apiVersion",
|
|
616
|
-
description=(
|
|
616
|
+
description=('Version of the schema the FieldPath is written in terms of, defaults to "v1".'),
|
|
617
617
|
),
|
|
618
618
|
] = None
|
|
619
619
|
field_path: Annotated[
|
|
@@ -729,15 +729,11 @@ class OwnerReference(BaseModel):
|
|
|
729
729
|
]
|
|
730
730
|
name: Annotated[
|
|
731
731
|
str,
|
|
732
|
-
Field(
|
|
733
|
-
description=("Name of the referent. More info:" " http://kubernetes.io/docs/user-guide/identifiers#names")
|
|
734
|
-
),
|
|
732
|
+
Field(description=("Name of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#names")),
|
|
735
733
|
]
|
|
736
734
|
uid: Annotated[
|
|
737
735
|
str,
|
|
738
|
-
Field(
|
|
739
|
-
description=("UID of the referent. More info:" " http://kubernetes.io/docs/user-guide/identifiers#uids")
|
|
740
|
-
),
|
|
736
|
+
Field(description=("UID of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#uids")),
|
|
741
737
|
]
|
|
742
738
|
|
|
743
739
|
|
|
@@ -757,7 +753,7 @@ class PersistentVolumeClaimVolumeSource(BaseModel):
|
|
|
757
753
|
Optional[bool],
|
|
758
754
|
Field(
|
|
759
755
|
alias="readOnly",
|
|
760
|
-
description=("readOnly Will force the ReadOnly setting in VolumeMounts. Default
|
|
756
|
+
description=("readOnly Will force the ReadOnly setting in VolumeMounts. Default false."),
|
|
761
757
|
),
|
|
762
758
|
] = None
|
|
763
759
|
|
|
@@ -826,9 +822,7 @@ class PreferredSchedulingTerm(BaseModel):
|
|
|
826
822
|
]
|
|
827
823
|
weight: Annotated[
|
|
828
824
|
int,
|
|
829
|
-
Field(
|
|
830
|
-
description=("Weight associated with matching the corresponding nodeSelectorTerm, in" " the range 1-100.")
|
|
831
|
-
),
|
|
825
|
+
Field(description=("Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100.")),
|
|
832
826
|
]
|
|
833
827
|
|
|
834
828
|
|
|
@@ -872,7 +866,7 @@ class QuobyteVolumeSource(BaseModel):
|
|
|
872
866
|
] = None
|
|
873
867
|
volume: Annotated[
|
|
874
868
|
str,
|
|
875
|
-
Field(description=("volume is a string that references an already created Quobyte volume
|
|
869
|
+
Field(description=("volume is a string that references an already created Quobyte volume by name.")),
|
|
876
870
|
]
|
|
877
871
|
|
|
878
872
|
|
|
@@ -894,8 +888,7 @@ class RBDVolumeSource(BaseModel):
|
|
|
894
888
|
str,
|
|
895
889
|
Field(
|
|
896
890
|
description=(
|
|
897
|
-
"image is the rados image name. More info:"
|
|
898
|
-
" https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it"
|
|
891
|
+
"image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it"
|
|
899
892
|
)
|
|
900
893
|
),
|
|
901
894
|
]
|
|
@@ -999,9 +992,7 @@ class ScaleIOVolumeSource(BaseModel):
|
|
|
999
992
|
Optional[str],
|
|
1000
993
|
Field(
|
|
1001
994
|
alias="protectionDomain",
|
|
1002
|
-
description=(
|
|
1003
|
-
"protectionDomain is the name of the ScaleIO Protection Domain for the" " configured storage."
|
|
1004
|
-
),
|
|
995
|
+
description=("protectionDomain is the name of the ScaleIO Protection Domain for the configured storage."),
|
|
1005
996
|
),
|
|
1006
997
|
] = None
|
|
1007
998
|
read_only: Annotated[
|
|
@@ -1029,7 +1020,7 @@ class ScaleIOVolumeSource(BaseModel):
|
|
|
1029
1020
|
Optional[bool],
|
|
1030
1021
|
Field(
|
|
1031
1022
|
alias="sslEnabled",
|
|
1032
|
-
description=("sslEnabled Flag enable/disable SSL communication with Gateway, default
|
|
1023
|
+
description=("sslEnabled Flag enable/disable SSL communication with Gateway, default false"),
|
|
1033
1024
|
),
|
|
1034
1025
|
] = None
|
|
1035
1026
|
storage_mode: Annotated[
|
|
@@ -1046,7 +1037,7 @@ class ScaleIOVolumeSource(BaseModel):
|
|
|
1046
1037
|
Optional[str],
|
|
1047
1038
|
Field(
|
|
1048
1039
|
alias="storagePool",
|
|
1049
|
-
description=("storagePool is the ScaleIO Storage Pool associated with the protection
|
|
1040
|
+
description=("storagePool is the ScaleIO Storage Pool associated with the protection domain."),
|
|
1050
1041
|
),
|
|
1051
1042
|
] = None
|
|
1052
1043
|
system: Annotated[
|
|
@@ -1231,7 +1222,7 @@ class ServiceAccountTokenProjection(BaseModel):
|
|
|
1231
1222
|
] = None
|
|
1232
1223
|
path: Annotated[
|
|
1233
1224
|
str,
|
|
1234
|
-
Field(description=("path is the path relative to the mount point of the file to project
|
|
1225
|
+
Field(description=("path is the path relative to the mount point of the file to project the token into.")),
|
|
1235
1226
|
]
|
|
1236
1227
|
|
|
1237
1228
|
|
|
@@ -1396,7 +1387,7 @@ class VolumeDevice(BaseModel):
|
|
|
1396
1387
|
str,
|
|
1397
1388
|
Field(
|
|
1398
1389
|
alias="devicePath",
|
|
1399
|
-
description=("devicePath is the path inside of the container that the device will be
|
|
1390
|
+
description=("devicePath is the path inside of the container that the device will be mapped to."),
|
|
1400
1391
|
),
|
|
1401
1392
|
]
|
|
1402
1393
|
name: Annotated[
|
|
@@ -1410,7 +1401,7 @@ class VolumeMount(BaseModel):
|
|
|
1410
1401
|
str,
|
|
1411
1402
|
Field(
|
|
1412
1403
|
alias="mountPath",
|
|
1413
|
-
description=("Path within the container at which the volume should be mounted. Must
|
|
1404
|
+
description=("Path within the container at which the volume should be mounted. Must not contain ':'."),
|
|
1414
1405
|
),
|
|
1415
1406
|
]
|
|
1416
1407
|
mount_propagation: Annotated[
|
|
@@ -1429,9 +1420,7 @@ class VolumeMount(BaseModel):
|
|
|
1429
1420
|
Optional[bool],
|
|
1430
1421
|
Field(
|
|
1431
1422
|
alias="readOnly",
|
|
1432
|
-
description=(
|
|
1433
|
-
"Mounted read-only if true, read-write otherwise (false or" " unspecified). Defaults to false."
|
|
1434
|
-
),
|
|
1423
|
+
description=("Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false."),
|
|
1435
1424
|
),
|
|
1436
1425
|
] = None
|
|
1437
1426
|
sub_path: Annotated[
|
|
@@ -1485,7 +1474,7 @@ class VsphereVirtualDiskVolumeSource(BaseModel):
|
|
|
1485
1474
|
Optional[str],
|
|
1486
1475
|
Field(
|
|
1487
1476
|
alias="storagePolicyName",
|
|
1488
|
-
description=("storagePolicyName is the storage Policy Based Management (SPBM)
|
|
1477
|
+
description=("storagePolicyName is the storage Policy Based Management (SPBM) profile name."),
|
|
1489
1478
|
),
|
|
1490
1479
|
] = None
|
|
1491
1480
|
volume_path: Annotated[
|
|
@@ -1589,7 +1578,7 @@ class CSIVolumeSource(BaseModel):
|
|
|
1589
1578
|
Field(
|
|
1590
1579
|
alias="readOnly",
|
|
1591
1580
|
description=(
|
|
1592
|
-
"readOnly specifies a read-only configuration for the volume. Defaults
|
|
1581
|
+
"readOnly specifies a read-only configuration for the volume. Defaults to false (read/write)."
|
|
1593
1582
|
),
|
|
1594
1583
|
),
|
|
1595
1584
|
] = None
|
|
@@ -1619,7 +1608,7 @@ class CephFSVolumeSource(BaseModel):
|
|
|
1619
1608
|
path: Annotated[
|
|
1620
1609
|
Optional[str],
|
|
1621
1610
|
Field(
|
|
1622
|
-
description=("path is Optional: Used as the mounted root, rather than the full Ceph
|
|
1611
|
+
description=("path is Optional: Used as the mounted root, rather than the full Ceph tree, default is /")
|
|
1623
1612
|
),
|
|
1624
1613
|
] = None
|
|
1625
1614
|
read_only: Annotated[
|
|
@@ -1695,8 +1684,7 @@ class CinderVolumeSource(BaseModel):
|
|
|
1695
1684
|
Field(
|
|
1696
1685
|
alias="secretRef",
|
|
1697
1686
|
description=(
|
|
1698
|
-
"secretRef is optional: points to a secret object containing parameters"
|
|
1699
|
-
" used to connect to OpenStack."
|
|
1687
|
+
"secretRef is optional: points to a secret object containing parameters used to connect to OpenStack."
|
|
1700
1688
|
),
|
|
1701
1689
|
),
|
|
1702
1690
|
] = None
|
|
@@ -1825,9 +1813,7 @@ class EnvFromSource(BaseModel):
|
|
|
1825
1813
|
] = None
|
|
1826
1814
|
prefix: Annotated[
|
|
1827
1815
|
Optional[str],
|
|
1828
|
-
Field(
|
|
1829
|
-
description=("An optional identifier to prepend to each key in the ConfigMap. Must" " be a C_IDENTIFIER.")
|
|
1830
|
-
),
|
|
1816
|
+
Field(description=("An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.")),
|
|
1831
1817
|
] = None
|
|
1832
1818
|
secret_ref: Annotated[
|
|
1833
1819
|
Optional[SecretEnvSource],
|
|
@@ -1918,14 +1904,14 @@ class ISCSIVolumeSource(BaseModel):
|
|
|
1918
1904
|
Optional[bool],
|
|
1919
1905
|
Field(
|
|
1920
1906
|
alias="chapAuthDiscovery",
|
|
1921
|
-
description=("chapAuthDiscovery defines whether support iSCSI Discovery CHAP
|
|
1907
|
+
description=("chapAuthDiscovery defines whether support iSCSI Discovery CHAP authentication"),
|
|
1922
1908
|
),
|
|
1923
1909
|
] = None
|
|
1924
1910
|
chap_auth_session: Annotated[
|
|
1925
1911
|
Optional[bool],
|
|
1926
1912
|
Field(
|
|
1927
1913
|
alias="chapAuthSession",
|
|
1928
|
-
description=("chapAuthSession defines whether support iSCSI Session CHAP
|
|
1914
|
+
description=("chapAuthSession defines whether support iSCSI Session CHAP authentication"),
|
|
1929
1915
|
),
|
|
1930
1916
|
] = None
|
|
1931
1917
|
fs_type: Annotated[
|
|
@@ -1958,7 +1944,7 @@ class ISCSIVolumeSource(BaseModel):
|
|
|
1958
1944
|
Field(
|
|
1959
1945
|
alias="iscsiInterface",
|
|
1960
1946
|
description=(
|
|
1961
|
-
"iscsiInterface is the interface Name that uses an iSCSI transport.
|
|
1947
|
+
"iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp)."
|
|
1962
1948
|
),
|
|
1963
1949
|
),
|
|
1964
1950
|
] = None
|
|
@@ -1977,14 +1963,14 @@ class ISCSIVolumeSource(BaseModel):
|
|
|
1977
1963
|
Optional[bool],
|
|
1978
1964
|
Field(
|
|
1979
1965
|
alias="readOnly",
|
|
1980
|
-
description=("readOnly here will force the ReadOnly setting in VolumeMounts.
|
|
1966
|
+
description=("readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false."),
|
|
1981
1967
|
),
|
|
1982
1968
|
] = None
|
|
1983
1969
|
secret_ref: Annotated[
|
|
1984
1970
|
Optional[LocalObjectReference],
|
|
1985
1971
|
Field(
|
|
1986
1972
|
alias="secretRef",
|
|
1987
|
-
description=("secretRef is the CHAP Secret for iSCSI target and initiator
|
|
1973
|
+
description=("secretRef is the CHAP Secret for iSCSI target and initiator authentication"),
|
|
1988
1974
|
),
|
|
1989
1975
|
] = None
|
|
1990
1976
|
target_portal: Annotated[
|
|
@@ -2005,7 +1991,7 @@ class LabelSelector(BaseModel):
|
|
|
2005
1991
|
Optional[List[LabelSelectorRequirement]],
|
|
2006
1992
|
Field(
|
|
2007
1993
|
alias="matchExpressions",
|
|
2008
|
-
description=("matchExpressions is a list of label selector requirements. The
|
|
1994
|
+
description=("matchExpressions is a list of label selector requirements. The requirements are ANDed."),
|
|
2009
1995
|
),
|
|
2010
1996
|
] = None
|
|
2011
1997
|
match_labels: Annotated[
|
|
@@ -2072,7 +2058,7 @@ class ManagedFieldsEntry(BaseModel):
|
|
|
2072
2058
|
Optional[FieldsV1],
|
|
2073
2059
|
Field(
|
|
2074
2060
|
alias="fieldsV1",
|
|
2075
|
-
description=(
|
|
2061
|
+
description=('FieldsV1 holds the first JSON version format as described in the "FieldsV1" type.'),
|
|
2076
2062
|
),
|
|
2077
2063
|
] = None
|
|
2078
2064
|
manager: Annotated[
|
|
@@ -2342,7 +2328,7 @@ class ObjectMeta(BaseModel):
|
|
|
2342
2328
|
Field(
|
|
2343
2329
|
alias="selfLink",
|
|
2344
2330
|
description=(
|
|
2345
|
-
"Deprecated: selfLink is a legacy read-only field that is no longer
|
|
2331
|
+
"Deprecated: selfLink is a legacy read-only field that is no longer populated by the system."
|
|
2346
2332
|
),
|
|
2347
2333
|
),
|
|
2348
2334
|
] = None
|
|
@@ -2372,12 +2358,12 @@ class PersistentVolumeClaimCondition(BaseModel):
|
|
|
2372
2358
|
Optional[Time],
|
|
2373
2359
|
Field(
|
|
2374
2360
|
alias="lastTransitionTime",
|
|
2375
|
-
description=("lastTransitionTime is the time the condition transitioned from one
|
|
2361
|
+
description=("lastTransitionTime is the time the condition transitioned from one status to another."),
|
|
2376
2362
|
),
|
|
2377
2363
|
] = None
|
|
2378
2364
|
message: Annotated[
|
|
2379
2365
|
Optional[str],
|
|
2380
|
-
Field(description=("message is the human-readable message indicating details about last
|
|
2366
|
+
Field(description=("message is the human-readable message indicating details about last transition.")),
|
|
2381
2367
|
] = None
|
|
2382
2368
|
reason: Annotated[
|
|
2383
2369
|
Optional[str],
|
|
@@ -2754,7 +2740,7 @@ class Probe(BaseModel):
|
|
|
2754
2740
|
Optional[int],
|
|
2755
2741
|
Field(
|
|
2756
2742
|
alias="periodSeconds",
|
|
2757
|
-
description=("How often (in seconds) to perform the probe. Default to 10 seconds.
|
|
2743
|
+
description=("How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1."),
|
|
2758
2744
|
),
|
|
2759
2745
|
] = None
|
|
2760
2746
|
success_threshold: Annotated[
|
|
@@ -2996,14 +2982,12 @@ class WeightedPodAffinityTerm(BaseModel):
|
|
|
2996
2982
|
PodAffinityTerm,
|
|
2997
2983
|
Field(
|
|
2998
2984
|
alias="podAffinityTerm",
|
|
2999
|
-
description=("Required. A pod affinity term, associated with the corresponding
|
|
2985
|
+
description=("Required. A pod affinity term, associated with the corresponding weight."),
|
|
3000
2986
|
),
|
|
3001
2987
|
]
|
|
3002
2988
|
weight: Annotated[
|
|
3003
2989
|
int,
|
|
3004
|
-
Field(
|
|
3005
|
-
description=("weight associated with matching the corresponding podAffinityTerm, in" " the range 1-100.")
|
|
3006
|
-
),
|
|
2990
|
+
Field(description=("weight associated with matching the corresponding podAffinityTerm, in the range 1-100.")),
|
|
3007
2991
|
]
|
|
3008
2992
|
|
|
3009
2993
|
|
|
@@ -3013,7 +2997,7 @@ class DownwardAPIVolumeFile(BaseModel):
|
|
|
3013
2997
|
Field(
|
|
3014
2998
|
alias="fieldRef",
|
|
3015
2999
|
description=(
|
|
3016
|
-
"Required: Selects a field of the pod: only annotations, labels, name
|
|
3000
|
+
"Required: Selects a field of the pod: only annotations, labels, name and namespace are supported."
|
|
3017
3001
|
),
|
|
3018
3002
|
),
|
|
3019
3003
|
] = None
|
|
@@ -3279,7 +3263,7 @@ class PersistentVolumeClaimSpec(BaseModel):
|
|
|
3279
3263
|
Optional[str],
|
|
3280
3264
|
Field(
|
|
3281
3265
|
alias="volumeName",
|
|
3282
|
-
description=("volumeName is the binding reference to the PersistentVolume backing
|
|
3266
|
+
description=("volumeName is the binding reference to the PersistentVolume backing this claim."),
|
|
3283
3267
|
),
|
|
3284
3268
|
] = None
|
|
3285
3269
|
|
|
@@ -3442,7 +3426,7 @@ class EnvVar(BaseModel):
|
|
|
3442
3426
|
Optional[EnvVarSource],
|
|
3443
3427
|
Field(
|
|
3444
3428
|
alias="valueFrom",
|
|
3445
|
-
description=("Source for the environment variable's value. Cannot be used if value
|
|
3429
|
+
description=("Source for the environment variable's value. Cannot be used if value is not empty."),
|
|
3446
3430
|
),
|
|
3447
3431
|
] = None
|
|
3448
3432
|
|
|
@@ -3552,7 +3536,7 @@ class VolumeProjection(BaseModel):
|
|
|
3552
3536
|
Optional[ServiceAccountTokenProjection],
|
|
3553
3537
|
Field(
|
|
3554
3538
|
alias="serviceAccountToken",
|
|
3555
|
-
description=("serviceAccountToken is information about the serviceAccountToken data
|
|
3539
|
+
description=("serviceAccountToken is information about the serviceAccountToken data to project"),
|
|
3556
3540
|
),
|
|
3557
3541
|
] = None
|
|
3558
3542
|
|
|
@@ -3593,7 +3577,7 @@ class Container(BaseModel):
|
|
|
3593
3577
|
] = None
|
|
3594
3578
|
env: Annotated[
|
|
3595
3579
|
Optional[List[EnvVar]],
|
|
3596
|
-
Field(description=("List of environment variables to set in the container. Cannot be
|
|
3580
|
+
Field(description=("List of environment variables to set in the container. Cannot be updated.")),
|
|
3597
3581
|
] = None
|
|
3598
3582
|
env_from: Annotated[
|
|
3599
3583
|
Optional[List[EnvFromSource]],
|
|
@@ -3795,14 +3779,14 @@ class Container(BaseModel):
|
|
|
3795
3779
|
Optional[List[VolumeDevice]],
|
|
3796
3780
|
Field(
|
|
3797
3781
|
alias="volumeDevices",
|
|
3798
|
-
description=("volumeDevices is the list of block devices to be used by the
|
|
3782
|
+
description=("volumeDevices is the list of block devices to be used by the container."),
|
|
3799
3783
|
),
|
|
3800
3784
|
] = None
|
|
3801
3785
|
volume_mounts: Annotated[
|
|
3802
3786
|
Optional[List[VolumeMount]],
|
|
3803
3787
|
Field(
|
|
3804
3788
|
alias="volumeMounts",
|
|
3805
|
-
description=("Pod volumes to mount into the container's filesystem. Cannot be
|
|
3789
|
+
description=("Pod volumes to mount into the container's filesystem. Cannot be updated."),
|
|
3806
3790
|
),
|
|
3807
3791
|
] = None
|
|
3808
3792
|
working_dir: Annotated[
|
|
@@ -3856,19 +3840,19 @@ class Volume(BaseModel):
|
|
|
3856
3840
|
Optional[AzureDiskVolumeSource],
|
|
3857
3841
|
Field(
|
|
3858
3842
|
alias="azureDisk",
|
|
3859
|
-
description=("azureDisk represents an Azure Data Disk mount on the host and bind
|
|
3843
|
+
description=("azureDisk represents an Azure Data Disk mount on the host and bind mount to the pod."),
|
|
3860
3844
|
),
|
|
3861
3845
|
] = None
|
|
3862
3846
|
azure_file: Annotated[
|
|
3863
3847
|
Optional[AzureFileVolumeSource],
|
|
3864
3848
|
Field(
|
|
3865
3849
|
alias="azureFile",
|
|
3866
|
-
description=("azureFile represents an Azure File Service mount on the host and bind
|
|
3850
|
+
description=("azureFile represents an Azure File Service mount on the host and bind mount to the pod."),
|
|
3867
3851
|
),
|
|
3868
3852
|
] = None
|
|
3869
3853
|
cephfs: Annotated[
|
|
3870
3854
|
Optional[CephFSVolumeSource],
|
|
3871
|
-
Field(description=("cephFS represents a Ceph FS mount on the host that shares a pod's
|
|
3855
|
+
Field(description=("cephFS represents a Ceph FS mount on the host that shares a pod's lifetime")),
|
|
3872
3856
|
] = None
|
|
3873
3857
|
cinder: Annotated[
|
|
3874
3858
|
Optional[CinderVolumeSource],
|
|
@@ -3900,7 +3884,7 @@ class Volume(BaseModel):
|
|
|
3900
3884
|
Optional[DownwardAPIVolumeSource],
|
|
3901
3885
|
Field(
|
|
3902
3886
|
alias="downwardAPI",
|
|
3903
|
-
description=("downwardAPI represents downward API about the pod that should populate
|
|
3887
|
+
description=("downwardAPI represents downward API about the pod that should populate this volume"),
|
|
3904
3888
|
),
|
|
3905
3889
|
] = None
|
|
3906
3890
|
empty_dir: Annotated[
|
|
@@ -4065,18 +4049,16 @@ class Volume(BaseModel):
|
|
|
4065
4049
|
Optional[PortworxVolumeSource],
|
|
4066
4050
|
Field(
|
|
4067
4051
|
alias="portworxVolume",
|
|
4068
|
-
description=(
|
|
4069
|
-
"portworxVolume represents a portworx volume attached and mounted on" " kubelets host machine"
|
|
4070
|
-
),
|
|
4052
|
+
description=("portworxVolume represents a portworx volume attached and mounted on kubelets host machine"),
|
|
4071
4053
|
),
|
|
4072
4054
|
] = None
|
|
4073
4055
|
projected: Annotated[
|
|
4074
4056
|
Optional[ProjectedVolumeSource],
|
|
4075
|
-
Field(description=("projected items for all in one resources secrets, configmaps, and
|
|
4057
|
+
Field(description=("projected items for all in one resources secrets, configmaps, and downward API")),
|
|
4076
4058
|
] = None
|
|
4077
4059
|
quobyte: Annotated[
|
|
4078
4060
|
Optional[QuobyteVolumeSource],
|
|
4079
|
-
Field(description=("quobyte represents a Quobyte mount on the host that shares a pod's
|
|
4061
|
+
Field(description=("quobyte represents a Quobyte mount on the host that shares a pod's lifetime")),
|
|
4080
4062
|
] = None
|
|
4081
4063
|
rbd: Annotated[
|
|
4082
4064
|
Optional[RBDVolumeSource],
|
|
@@ -4092,9 +4074,7 @@ class Volume(BaseModel):
|
|
|
4092
4074
|
Optional[ScaleIOVolumeSource],
|
|
4093
4075
|
Field(
|
|
4094
4076
|
alias="scaleIO",
|
|
4095
|
-
description=(
|
|
4096
|
-
"scaleIO represents a ScaleIO persistent volume attached and mounted on" " Kubernetes nodes."
|
|
4097
|
-
),
|
|
4077
|
+
description=("scaleIO represents a ScaleIO persistent volume attached and mounted on Kubernetes nodes."),
|
|
4098
4078
|
),
|
|
4099
4079
|
] = None
|
|
4100
4080
|
secret: Annotated[
|
|
@@ -4108,12 +4088,12 @@ class Volume(BaseModel):
|
|
|
4108
4088
|
] = None
|
|
4109
4089
|
storageos: Annotated[
|
|
4110
4090
|
Optional[StorageOSVolumeSource],
|
|
4111
|
-
Field(description=("storageOS represents a StorageOS volume attached and mounted on
|
|
4091
|
+
Field(description=("storageOS represents a StorageOS volume attached and mounted on Kubernetes nodes.")),
|
|
4112
4092
|
] = None
|
|
4113
4093
|
vsphere_volume: Annotated[
|
|
4114
4094
|
Optional[VsphereVirtualDiskVolumeSource],
|
|
4115
4095
|
Field(
|
|
4116
4096
|
alias="vsphereVolume",
|
|
4117
|
-
description=("vsphereVolume represents a vSphere volume attached and mounted on
|
|
4097
|
+
description=("vsphereVolume represents a vSphere volume attached and mounted on kubelets host machine"),
|
|
4118
4098
|
),
|
|
4119
4099
|
] = None
|
|
@@ -581,7 +581,7 @@ class ContainerNode(BaseModel):
|
|
|
581
581
|
dependencies: Optional[List[str]] = None
|
|
582
582
|
env: Annotated[
|
|
583
583
|
Optional[List[v1.EnvVar]],
|
|
584
|
-
Field(description=("List of environment variables to set in the container. Cannot be
|
|
584
|
+
Field(description=("List of environment variables to set in the container. Cannot be updated.")),
|
|
585
585
|
] = None
|
|
586
586
|
env_from: Annotated[
|
|
587
587
|
Optional[List[v1.EnvFromSource]],
|
|
@@ -783,14 +783,14 @@ class ContainerNode(BaseModel):
|
|
|
783
783
|
Optional[List[v1.VolumeDevice]],
|
|
784
784
|
Field(
|
|
785
785
|
alias="volumeDevices",
|
|
786
|
-
description=("volumeDevices is the list of block devices to be used by the
|
|
786
|
+
description=("volumeDevices is the list of block devices to be used by the container."),
|
|
787
787
|
),
|
|
788
788
|
] = None
|
|
789
789
|
volume_mounts: Annotated[
|
|
790
790
|
Optional[List[v1.VolumeMount]],
|
|
791
791
|
Field(
|
|
792
792
|
alias="volumeMounts",
|
|
793
|
-
description=("Pod volumes to mount into the container's filesystem. Cannot be
|
|
793
|
+
description=("Pod volumes to mount into the container's filesystem. Cannot be updated."),
|
|
794
794
|
),
|
|
795
795
|
] = None
|
|
796
796
|
working_dir: Annotated[
|
|
@@ -854,7 +854,7 @@ class ScriptTemplate(BaseModel):
|
|
|
854
854
|
] = None
|
|
855
855
|
env: Annotated[
|
|
856
856
|
Optional[List[v1.EnvVar]],
|
|
857
|
-
Field(description=("List of environment variables to set in the container. Cannot be
|
|
857
|
+
Field(description=("List of environment variables to set in the container. Cannot be updated.")),
|
|
858
858
|
] = None
|
|
859
859
|
env_from: Annotated[
|
|
860
860
|
Optional[List[v1.EnvFromSource]],
|
|
@@ -1057,14 +1057,14 @@ class ScriptTemplate(BaseModel):
|
|
|
1057
1057
|
Optional[List[v1.VolumeDevice]],
|
|
1058
1058
|
Field(
|
|
1059
1059
|
alias="volumeDevices",
|
|
1060
|
-
description=("volumeDevices is the list of block devices to be used by the
|
|
1060
|
+
description=("volumeDevices is the list of block devices to be used by the container."),
|
|
1061
1061
|
),
|
|
1062
1062
|
] = None
|
|
1063
1063
|
volume_mounts: Annotated[
|
|
1064
1064
|
Optional[List[v1.VolumeMount]],
|
|
1065
1065
|
Field(
|
|
1066
1066
|
alias="volumeMounts",
|
|
1067
|
-
description=("Pod volumes to mount into the container's filesystem. Cannot be
|
|
1067
|
+
description=("Pod volumes to mount into the container's filesystem. Cannot be updated."),
|
|
1068
1068
|
),
|
|
1069
1069
|
] = None
|
|
1070
1070
|
working_dir: Annotated[
|
|
@@ -1116,7 +1116,7 @@ class UserContainer(BaseModel):
|
|
|
1116
1116
|
] = None
|
|
1117
1117
|
env: Annotated[
|
|
1118
1118
|
Optional[List[v1.EnvVar]],
|
|
1119
|
-
Field(description=("List of environment variables to set in the container. Cannot be
|
|
1119
|
+
Field(description=("List of environment variables to set in the container. Cannot be updated.")),
|
|
1120
1120
|
] = None
|
|
1121
1121
|
env_from: Annotated[
|
|
1122
1122
|
Optional[List[v1.EnvFromSource]],
|
|
@@ -1319,14 +1319,14 @@ class UserContainer(BaseModel):
|
|
|
1319
1319
|
Optional[List[v1.VolumeDevice]],
|
|
1320
1320
|
Field(
|
|
1321
1321
|
alias="volumeDevices",
|
|
1322
|
-
description=("volumeDevices is the list of block devices to be used by the
|
|
1322
|
+
description=("volumeDevices is the list of block devices to be used by the container."),
|
|
1323
1323
|
),
|
|
1324
1324
|
] = None
|
|
1325
1325
|
volume_mounts: Annotated[
|
|
1326
1326
|
Optional[List[v1.VolumeMount]],
|
|
1327
1327
|
Field(
|
|
1328
1328
|
alias="volumeMounts",
|
|
1329
|
-
description=("Pod volumes to mount into the container's filesystem. Cannot be
|
|
1329
|
+
description=("Pod volumes to mount into the container's filesystem. Cannot be updated."),
|
|
1330
1330
|
),
|
|
1331
1331
|
] = None
|
|
1332
1332
|
working_dir: Annotated[
|
|
@@ -2,19 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
-
from
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Generator, List, Optional, cast
|
|
6
9
|
|
|
10
|
+
import jwt
|
|
7
11
|
import requests
|
|
8
12
|
from hera.workflows import CronWorkflow, Workflow
|
|
9
|
-
from hera.workflows.models import
|
|
13
|
+
from hera.workflows.models import CronWorkflow as CronWorkflowModel
|
|
14
|
+
from hera.workflows.models import UpdateCronWorkflowRequest, WorkflowCreateRequest
|
|
15
|
+
from pydantic import BaseModel, ValidationError
|
|
10
16
|
|
|
11
17
|
from pipekit_sdk._helpers import is_valid_uuid
|
|
12
18
|
from pipekit_sdk.models.model import Cluster, Logs, Pipe, PipeRun
|
|
13
19
|
|
|
20
|
+
_CONTENT_TYPE_JSON = "application/json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _LoginResponse(BaseModel):
|
|
24
|
+
"""LoginResponse is a model for Login responses."""
|
|
25
|
+
|
|
26
|
+
token_type: str
|
|
27
|
+
access_token: str
|
|
28
|
+
expires_in: int
|
|
29
|
+
refresh_token: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _AccessToken(BaseModel):
|
|
33
|
+
"""AccessToken is a model for decoded access tokens."""
|
|
34
|
+
|
|
35
|
+
identityUUID: str
|
|
36
|
+
ClusterUUID: str
|
|
37
|
+
OrgUUID: str
|
|
38
|
+
isActive: bool
|
|
39
|
+
products: List[str]
|
|
40
|
+
iss: str
|
|
41
|
+
sub: str
|
|
42
|
+
exp: int
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _ValidationResult(Enum):
|
|
46
|
+
OK = 0
|
|
47
|
+
INVALID_TOKEN = 1
|
|
48
|
+
EXPIRED_TOKEN = 2
|
|
49
|
+
|
|
14
50
|
|
|
15
51
|
class PipekitService:
|
|
16
52
|
"""PipekitService is a wrapper around the Pipekit API. It provides a simple interface to interact with Pipekit."""
|
|
17
53
|
|
|
54
|
+
@staticmethod
|
|
55
|
+
def __from_home_dir() -> Optional[_LoginResponse]:
|
|
56
|
+
home = Path.home()
|
|
57
|
+
pipekit_token_file_path = os.path.join(home, ".pipekit", "token")
|
|
58
|
+
try:
|
|
59
|
+
with open(pipekit_token_file_path, "r") as f:
|
|
60
|
+
raw_data = f.read()
|
|
61
|
+
return _LoginResponse.model_validate_json(raw_data)
|
|
62
|
+
except ValidationError:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def __write_home_dir(login_resp: _LoginResponse):
|
|
67
|
+
home = Path.home()
|
|
68
|
+
pipekit_token_file_path = os.path.join(home, ".pipekit", "token")
|
|
69
|
+
with open(pipekit_token_file_path, "w") as f:
|
|
70
|
+
payload = login_resp.model_dump_json(indent=2)
|
|
71
|
+
f.write(payload)
|
|
72
|
+
|
|
18
73
|
def __init__(
|
|
19
74
|
self,
|
|
20
75
|
username: str = "",
|
|
@@ -38,8 +93,60 @@ class PipekitService:
|
|
|
38
93
|
self.access_token = token
|
|
39
94
|
return
|
|
40
95
|
|
|
96
|
+
maybe_resp = self.__from_home_dir()
|
|
97
|
+
self.token_data = maybe_resp
|
|
98
|
+
|
|
99
|
+
if self.token_data is not None:
|
|
100
|
+
self.__token_login_flow()
|
|
101
|
+
return
|
|
102
|
+
|
|
41
103
|
self.__login()
|
|
42
104
|
|
|
105
|
+
def __update_token(self) -> None:
|
|
106
|
+
login_url = f"{self.id_url}/api/id/v1/oauth/token"
|
|
107
|
+
assert self.token_data is not None, "Token data was None, this implies a bug"
|
|
108
|
+
login_request = {"grant_type": "refresh_token", "refresh_token": self.token_data.refresh_token}
|
|
109
|
+
login_response = requests.post(
|
|
110
|
+
login_url,
|
|
111
|
+
data=json.dumps(login_request),
|
|
112
|
+
headers={"Content-Type": _CONTENT_TYPE_JSON},
|
|
113
|
+
timeout=10,
|
|
114
|
+
)
|
|
115
|
+
if login_response.status_code // 100 != 2:
|
|
116
|
+
raise Exception(
|
|
117
|
+
"Couldn't refresh access token, obtained non 2xx status code, try logging in again via the cli"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
login_response_model = _LoginResponse.model_validate(login_response.json())
|
|
121
|
+
self.__write_home_dir(login_response_model)
|
|
122
|
+
self.access_token = login_response_model.access_token
|
|
123
|
+
|
|
124
|
+
def __validate_token(self) -> _ValidationResult:
|
|
125
|
+
assert self.token_data is not None
|
|
126
|
+
try:
|
|
127
|
+
decoded_payload = jwt.decode(self.token_data.access_token, options={"verify_signature": False})
|
|
128
|
+
acc_tk = _AccessToken.model_validate(decoded_payload)
|
|
129
|
+
exp_time = datetime.fromtimestamp(acc_tk.exp)
|
|
130
|
+
now_time = datetime.now()
|
|
131
|
+
|
|
132
|
+
if exp_time <= now_time:
|
|
133
|
+
return _ValidationResult.EXPIRED_TOKEN
|
|
134
|
+
except jwt.PyJWTError:
|
|
135
|
+
return _ValidationResult.INVALID_TOKEN
|
|
136
|
+
return _ValidationResult.OK
|
|
137
|
+
|
|
138
|
+
def __token_login_flow(self) -> None:
|
|
139
|
+
assert self.token_data is not None, "token was None when it was expected to be valid"
|
|
140
|
+
res = self.__validate_token()
|
|
141
|
+
if res == _ValidationResult.EXPIRED_TOKEN:
|
|
142
|
+
self.__update_token()
|
|
143
|
+
elif res == _ValidationResult.INVALID_TOKEN:
|
|
144
|
+
raise RuntimeError(
|
|
145
|
+
"Token was invalid, if the `.pipekit/token` file is corrupted, please delete the file and login again"
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
self.access_token = self.token_data.access_token
|
|
149
|
+
|
|
43
150
|
def __login(self) -> None:
|
|
44
151
|
login_request = {
|
|
45
152
|
"username": self.username,
|
|
@@ -55,7 +162,7 @@ class PipekitService:
|
|
|
55
162
|
login_response = requests.post(
|
|
56
163
|
login_url,
|
|
57
164
|
data=json.dumps(login_request),
|
|
58
|
-
headers={"Content-Type":
|
|
165
|
+
headers={"Content-Type": _CONTENT_TYPE_JSON},
|
|
59
166
|
timeout=10,
|
|
60
167
|
)
|
|
61
168
|
|
|
@@ -255,6 +362,14 @@ class PipekitService:
|
|
|
255
362
|
|
|
256
363
|
return None
|
|
257
364
|
|
|
365
|
+
def __resolve_cluster_uuid(self, cluster: str) -> str:
|
|
366
|
+
"""Return the cluster UUID for a cluster name, or pass through a UUID unchanged."""
|
|
367
|
+
if is_valid_uuid(cluster):
|
|
368
|
+
return cluster
|
|
369
|
+
if cluster_obj := self.get_cluster_by_name(cluster):
|
|
370
|
+
return cluster_obj.uuid
|
|
371
|
+
raise ValueError(f"No cluster found for {cluster}")
|
|
372
|
+
|
|
258
373
|
def submit(
|
|
259
374
|
self,
|
|
260
375
|
workflow: Workflow,
|
|
@@ -262,11 +377,7 @@ class PipekitService:
|
|
|
262
377
|
pipe_name: str = "",
|
|
263
378
|
) -> PipeRun:
|
|
264
379
|
"""Submit a Workflow to a cluster, under an existing or new pipe with the given name."""
|
|
265
|
-
|
|
266
|
-
if cluster_obj := self.get_cluster_by_name(cluster):
|
|
267
|
-
cluster = cluster_obj.uuid
|
|
268
|
-
else:
|
|
269
|
-
raise ValueError(f"No cluster found for {cluster}")
|
|
380
|
+
cluster = self.__resolve_cluster_uuid(cluster)
|
|
270
381
|
|
|
271
382
|
submit_url = f"{self.users_url}/api/users/v1/clusters/{cluster}/workflows"
|
|
272
383
|
if pipe_name != "":
|
|
@@ -285,7 +396,7 @@ class PipekitService:
|
|
|
285
396
|
),
|
|
286
397
|
headers={
|
|
287
398
|
"Authorization": f"Bearer {self.access_token}",
|
|
288
|
-
"Content-Type":
|
|
399
|
+
"Content-Type": _CONTENT_TYPE_JSON,
|
|
289
400
|
},
|
|
290
401
|
timeout=10,
|
|
291
402
|
)
|
|
@@ -297,11 +408,7 @@ class PipekitService:
|
|
|
297
408
|
|
|
298
409
|
def create(self, cron_workflow: CronWorkflow, cluster: str, pipe_name: str = "") -> PipeRun:
|
|
299
410
|
"""Create a CronWorkflow on the cluster, under an existing or new pipe with the given name."""
|
|
300
|
-
|
|
301
|
-
if cluster_obj := self.get_cluster_by_name(cluster):
|
|
302
|
-
cluster = cluster_obj.uuid
|
|
303
|
-
else:
|
|
304
|
-
raise ValueError(f"No cluster found for {cluster}")
|
|
411
|
+
cluster = self.__resolve_cluster_uuid(cluster)
|
|
305
412
|
|
|
306
413
|
create_url = f"{self.users_url}/api/users/v1/clusters/{cluster}/cron-workflows"
|
|
307
414
|
if pipe_name != "":
|
|
@@ -320,7 +427,7 @@ class PipekitService:
|
|
|
320
427
|
),
|
|
321
428
|
headers={
|
|
322
429
|
"Authorization": f"Bearer {self.access_token}",
|
|
323
|
-
"Content-Type":
|
|
430
|
+
"Content-Type": _CONTENT_TYPE_JSON,
|
|
324
431
|
},
|
|
325
432
|
timeout=10,
|
|
326
433
|
)
|
|
@@ -330,6 +437,129 @@ class PipekitService:
|
|
|
330
437
|
|
|
331
438
|
return PipeRun.model_validate(create_response.json())
|
|
332
439
|
|
|
440
|
+
@staticmethod
|
|
441
|
+
def __require_namespace(namespace: str) -> None:
|
|
442
|
+
# A cron lives in the namespace set on its manifest (the platform default
|
|
443
|
+
# is "argo"). An empty or wrong namespace makes the cron look missing, so
|
|
444
|
+
# we reject an empty namespace here instead of sending a request that
|
|
445
|
+
# returns a confusing "no rows" error.
|
|
446
|
+
if namespace == "":
|
|
447
|
+
raise ValueError(
|
|
448
|
+
"namespace is required and must match the CronWorkflow manifest namespace "
|
|
449
|
+
"(the platform default is 'argo')"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def __cron_workflow_url(self, cluster_uuid: str, namespace: str, name: str) -> str:
|
|
453
|
+
return f"{self.users_url}/api/users/v1/clusters/{cluster_uuid}/hera/api/v1/cron-workflows/{namespace}/{name}"
|
|
454
|
+
|
|
455
|
+
def __cron_request(self, method: str, url: str, action: str, data: Optional[str] = None) -> requests.Response:
|
|
456
|
+
# Shared HTTP call for the cron methods: attach auth, send, and turn a
|
|
457
|
+
# non-2xx status into an error. `action` names the operation in the
|
|
458
|
+
# error message (for example "Update").
|
|
459
|
+
headers = {"Authorization": f"Bearer {self.access_token}"}
|
|
460
|
+
if data is not None:
|
|
461
|
+
headers["Content-Type"] = _CONTENT_TYPE_JSON
|
|
462
|
+
|
|
463
|
+
response = requests.request(method, url, data=data, headers=headers, timeout=10)
|
|
464
|
+
if response.status_code // 100 != 2:
|
|
465
|
+
raise RuntimeError(f"{action} cron failed: {response.text}")
|
|
466
|
+
|
|
467
|
+
return response
|
|
468
|
+
|
|
469
|
+
def __cron_by_name(self, cluster: str, namespace: str, name: str) -> str:
|
|
470
|
+
# Validate the namespace, resolve the cluster name to a UUID, and return
|
|
471
|
+
# the cron URL. Shared by the methods that act on a cron by name.
|
|
472
|
+
self.__require_namespace(namespace)
|
|
473
|
+
cluster_uuid = self.__resolve_cluster_uuid(cluster)
|
|
474
|
+
return self.__cron_workflow_url(cluster_uuid, namespace, name)
|
|
475
|
+
|
|
476
|
+
def update_cron(
|
|
477
|
+
self,
|
|
478
|
+
cron_workflow: CronWorkflow,
|
|
479
|
+
cluster: str,
|
|
480
|
+
namespace: str = "",
|
|
481
|
+
) -> CronWorkflowModel:
|
|
482
|
+
"""Update an existing CronWorkflow's spec and return the updated object.
|
|
483
|
+
|
|
484
|
+
The namespace must match the one in the CronWorkflow manifest. When
|
|
485
|
+
namespace is empty, it is taken from the CronWorkflow object.
|
|
486
|
+
"""
|
|
487
|
+
if namespace == "":
|
|
488
|
+
namespace = cron_workflow.namespace or ""
|
|
489
|
+
|
|
490
|
+
name = cron_workflow.name
|
|
491
|
+
if not name:
|
|
492
|
+
raise ValueError("CronWorkflow metadata.name is required")
|
|
493
|
+
|
|
494
|
+
url = self.__cron_by_name(cluster, namespace, name)
|
|
495
|
+
|
|
496
|
+
built = cron_workflow.build()
|
|
497
|
+
assert built is not None
|
|
498
|
+
request = UpdateCronWorkflowRequest(
|
|
499
|
+
name=name,
|
|
500
|
+
namespace=namespace,
|
|
501
|
+
cron_workflow=built, # type: ignore
|
|
502
|
+
)
|
|
503
|
+
body = request.json(exclude_none=True, by_alias=True, exclude_unset=True, exclude_defaults=True)
|
|
504
|
+
|
|
505
|
+
response = self.__cron_request("PUT", url, "Update", data=body)
|
|
506
|
+
return CronWorkflowModel.parse_raw(response.text)
|
|
507
|
+
|
|
508
|
+
def delete_cron(
|
|
509
|
+
self,
|
|
510
|
+
cluster: str,
|
|
511
|
+
namespace: str,
|
|
512
|
+
name: str,
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Delete a CronWorkflow by cluster, namespace, and name.
|
|
515
|
+
|
|
516
|
+
The namespace must match the CronWorkflow manifest namespace.
|
|
517
|
+
"""
|
|
518
|
+
url = self.__cron_by_name(cluster, namespace, name)
|
|
519
|
+
self.__cron_request("DELETE", url, "Delete")
|
|
520
|
+
|
|
521
|
+
def get_cron(
|
|
522
|
+
self,
|
|
523
|
+
cluster: str,
|
|
524
|
+
namespace: str,
|
|
525
|
+
name: str,
|
|
526
|
+
) -> CronWorkflowModel:
|
|
527
|
+
"""Get a CronWorkflow by cluster, namespace, and name.
|
|
528
|
+
|
|
529
|
+
The namespace must match the CronWorkflow manifest namespace.
|
|
530
|
+
"""
|
|
531
|
+
url = self.__cron_by_name(cluster, namespace, name)
|
|
532
|
+
response = self.__cron_request("GET", url, "Get")
|
|
533
|
+
return CronWorkflowModel.parse_raw(response.text)
|
|
534
|
+
|
|
535
|
+
def suspend_cron(
|
|
536
|
+
self,
|
|
537
|
+
cluster: str,
|
|
538
|
+
namespace: str,
|
|
539
|
+
name: str,
|
|
540
|
+
) -> CronWorkflowModel:
|
|
541
|
+
"""Suspend a CronWorkflow so it stops scheduling new runs.
|
|
542
|
+
|
|
543
|
+
The namespace must match the CronWorkflow manifest namespace.
|
|
544
|
+
"""
|
|
545
|
+
url = self.__cron_by_name(cluster, namespace, name) + "/suspend"
|
|
546
|
+
response = self.__cron_request("PUT", url, "Suspend")
|
|
547
|
+
return CronWorkflowModel.parse_raw(response.text)
|
|
548
|
+
|
|
549
|
+
def resume_cron(
|
|
550
|
+
self,
|
|
551
|
+
cluster: str,
|
|
552
|
+
namespace: str,
|
|
553
|
+
name: str,
|
|
554
|
+
) -> CronWorkflowModel:
|
|
555
|
+
"""Resume a suspended CronWorkflow so it schedules runs again.
|
|
556
|
+
|
|
557
|
+
The namespace must match the CronWorkflow manifest namespace.
|
|
558
|
+
"""
|
|
559
|
+
url = self.__cron_by_name(cluster, namespace, name) + "/resume"
|
|
560
|
+
response = self.__cron_request("PUT", url, "Resume")
|
|
561
|
+
return CronWorkflowModel.parse_raw(response.text)
|
|
562
|
+
|
|
333
563
|
def __apply_run_action(
|
|
334
564
|
self,
|
|
335
565
|
run_uuid: str,
|
|
@@ -340,7 +570,7 @@ class PipekitService:
|
|
|
340
570
|
action_url,
|
|
341
571
|
headers={
|
|
342
572
|
"Authorization": f"Bearer {self.access_token}",
|
|
343
|
-
"Content-Type":
|
|
573
|
+
"Content-Type": _CONTENT_TYPE_JSON,
|
|
344
574
|
},
|
|
345
575
|
timeout=10,
|
|
346
576
|
)
|
|
@@ -374,13 +604,16 @@ class PipekitService:
|
|
|
374
604
|
def __get_run_container_logs(
|
|
375
605
|
self,
|
|
376
606
|
run_uuid: str,
|
|
377
|
-
pod_name: str =
|
|
378
|
-
container_name: str =
|
|
607
|
+
pod_name: List[str] | str = [],
|
|
608
|
+
container_name: List[str] | str = [],
|
|
379
609
|
) -> list[Logs]:
|
|
380
|
-
logs_url = f"{self.users_url}/api/users/v1/runs/{run_uuid}/container_logs
|
|
610
|
+
logs_url = f"{self.users_url}/api/users/v1/runs/{run_uuid}/container_logs"
|
|
381
611
|
|
|
612
|
+
# empty "" / [] must be omitted, not sent as ?pod-name= (the API filters to a pod named "")
|
|
613
|
+
params = {k: v for k, v in {"pod-name": pod_name, "container-name": container_name}.items() if v}
|
|
382
614
|
logs_response = requests.get(
|
|
383
615
|
logs_url,
|
|
616
|
+
params=params,
|
|
384
617
|
headers={"Authorization": f"Bearer {self.access_token}"},
|
|
385
618
|
timeout=10,
|
|
386
619
|
)
|
|
@@ -393,14 +626,17 @@ class PipekitService:
|
|
|
393
626
|
def __get_logs_stream(
|
|
394
627
|
self,
|
|
395
628
|
run_uuid: str,
|
|
396
|
-
pod_name: str = "",
|
|
397
|
-
container_name: str = "",
|
|
629
|
+
pod_name: List[str] | str = "",
|
|
630
|
+
container_name: List[str] | str = "",
|
|
398
631
|
last_event_id: str = "",
|
|
399
632
|
) -> requests.Response:
|
|
400
|
-
logs_url = f"{self.users_url}/api/users/v1/runs/{run_uuid}/container_logs_stream
|
|
633
|
+
logs_url = f"{self.users_url}/api/users/v1/runs/{run_uuid}/container_logs_stream"
|
|
401
634
|
|
|
635
|
+
# empty "" / [] must be omitted, not sent as ?pod-name= (the API filters to a pod named "")
|
|
636
|
+
params = {k: v for k, v in {"pod-name": pod_name, "container-name": container_name}.items() if v}
|
|
402
637
|
logs_response = requests.get(
|
|
403
638
|
logs_url,
|
|
639
|
+
params=params,
|
|
404
640
|
stream=True,
|
|
405
641
|
timeout=None, # nosec B113
|
|
406
642
|
headers={
|
|
@@ -419,8 +655,8 @@ class PipekitService:
|
|
|
419
655
|
def get_logs(
|
|
420
656
|
self,
|
|
421
657
|
run_uuid: str,
|
|
422
|
-
pod_name: str = "",
|
|
423
|
-
container_name: str = "",
|
|
658
|
+
pod_name: List[str] | str = "",
|
|
659
|
+
container_name: List[str] | str = "",
|
|
424
660
|
) -> list[Logs]:
|
|
425
661
|
"""Get logs for a given run."""
|
|
426
662
|
return self.__get_run_container_logs(run_uuid, pod_name, container_name)
|
|
@@ -428,8 +664,8 @@ class PipekitService:
|
|
|
428
664
|
def follow_logs(
|
|
429
665
|
self,
|
|
430
666
|
run_uuid: str,
|
|
431
|
-
pod_name: str = "",
|
|
432
|
-
container_name: str = "",
|
|
667
|
+
pod_name: List[str] | str = "",
|
|
668
|
+
container_name: List[str] | str = "",
|
|
433
669
|
) -> Generator[Logs, Any, None]:
|
|
434
670
|
"""Return a generator that yields pod logs for a given run."""
|
|
435
671
|
data_msg_prefix = "data: "
|
|
@@ -475,14 +711,19 @@ class PipekitService:
|
|
|
475
711
|
def print_logs(
|
|
476
712
|
self,
|
|
477
713
|
run_uuid: str,
|
|
478
|
-
pod_name: str = "",
|
|
479
|
-
container_name: str = "",
|
|
714
|
+
pod_name: List[str] | str = "",
|
|
715
|
+
container_name: List[str] | str = "",
|
|
480
716
|
follow: bool = True,
|
|
481
717
|
) -> None:
|
|
482
718
|
"""Print pod logs for a given run."""
|
|
483
719
|
if follow:
|
|
484
720
|
for log in self.follow_logs(run_uuid, pod_name=pod_name, container_name=container_name):
|
|
485
721
|
self.__print_log_message(log)
|
|
722
|
+
return
|
|
486
723
|
|
|
487
724
|
for log in self.get_logs(run_uuid, pod_name=pod_name, container_name=container_name):
|
|
488
725
|
self.__print_log_message(log)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
if __name__ == "__main__":
|
|
729
|
+
p = PipekitService()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|