diff --git a/html/ui/Cluster-bd053ffb62d5a804.js b/html/ui/Cluster-bd053ffb62d5a804.js
new file mode 100644
index 0000000..aa3badf
--- /dev/null
+++ b/html/ui/Cluster-bd053ffb62d5a804.js
@@ -0,0 +1,22 @@
+const Cluster = {
+ components: { ClusterAccess, ClusterCAs, Downloads, GetCopy },
+ props: [ 'cluster', 'token', 'state' ],
+ template: `
+
+
+
Tokens
+
+
+ Passwords
+
+
+ Downloads
+
+
+
+`
+}
diff --git a/html/ui/Cluster-ddf1029883a9a21d.js b/html/ui/ClusterAccess-fede0ff535b7cf.js
similarity index 50%
rename from html/ui/Cluster-ddf1029883a9a21d.js
rename to html/ui/ClusterAccess-fede0ff535b7cf.js
index 827a01f..0544790 100644
--- a/html/ui/Cluster-ddf1029883a9a21d.js
+++ b/html/ui/ClusterAccess-fede0ff535b7cf.js
@@ -1,8 +1,8 @@
-const Cluster = {
- components: { Downloads, GetCopy },
+const ClusterAccess = {
props: [ 'cluster', 'token', 'state' ],
data() {
return {
+ accessType: "ssh",
signReqValidity: "1d",
sshSignReq: {
PubKey: "",
@@ -16,8 +16,35 @@ const Cluster = {
},
kubeUserCert: null,
downloadSet: null,
+ selectedAssets: {},
};
},
+ computed: {
+ availableAssets() {
+ return [
+ "kernel",
+ "initrd",
+ "uki",
+ "bootstrap.tar",
+ "boot.img.gz",
+ "boot.img",
+ "boot.qcow2",
+ "boot.vmdk",
+ "boot.iso",
+ "boot.tar",
+ "bootstrap-config",
+ "config",
+ "config.json",
+ "ipxe",
+ ]
+ },
+ selectedAssetList() {
+ return this.availableAssets.filter(a => this.selectedAssets[a])
+ },
+ hostCount() {
+ return (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name).length
+ },
+ },
methods: {
sshCASign() {
event.preventDefault();
@@ -49,6 +76,28 @@ const Cluster = {
})
.catch((e) => { alert('failed to sign: '+e); })
},
+ generateDownloadSet() {
+ event.preventDefault()
+
+ const hosts = this.hostCount ? (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name) : []
+ const items = hosts.map(h => ({
+ Kind: "host",
+ Name: h.Name,
+ Assets: this.selectedAssetList,
+ }))
+
+ fetch('/sign-download-set', {
+ method: 'POST',
+ body: JSON.stringify({Expiry: this.signReqValidity, Items: items}),
+ headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
+ }).then((resp) => {
+ if (resp.ok) {
+ resp.json().then((set) => { this.downloadSet = set })
+ } else {
+ resp.json().then((resp) => alert('failed to generate: '+resp.message))
+ }
+ }).catch((e) => { alert('failed to generate: '+e) })
+ },
readFile(e, onload) {
const file = e.target.files[0];
if (!file) { return; }
@@ -67,97 +116,63 @@ const Cluster = {
this.kubeSignReq.CSR = v;
});
},
- generateDownloadSet() {
- event.preventDefault()
-
- const hosts = (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name)
- const items = hosts.map(h => ({
- Kind: "host",
- Name: h.Name,
- Assets: ["kernel", "initrd", "uki", "bootstrap.tar", "boot.img.gz", "boot.img", "boot.qcow2", "boot.iso", "boot.tar", "bootstrap-config", "config", "config.json", "ipxe"],
- }))
-
- fetch('/sign-download-set', {
- method: 'POST',
- body: JSON.stringify({Expiry: this.signReqValidity, Items: items}),
- headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
- }).then((resp) => {
- if (resp.ok) {
- resp.json().then((set) => { this.downloadSet = set })
- } else {
- resp.json().then((resp) => alert('failed to generate: '+resp.message))
- }
- }).catch((e) => { alert('failed to generate: '+e) })
- },
},
template: `
Access
- Allow cluster access from a public key
-
- Grant SSH access
+ Grant access to:
+
+
+
+
Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.
- User:
- Public key (OpenSSH format):
-
-
-
-
-
- => Get certificate
-
-
+
+
Grant SSH access
+
User:
+
Public key (OpenSSH format):
+
+
+
+
+
+ => Get certificate
+
+
+
- Grant Kubernetes API access
-
- Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.
- User: (by default, from the CSR)
- Group:
- Certificate signing request (PEM format):
-
-
-
-
-
-
- => Get certificate
-
-
-
- Tokens
-
-
- Passwords
-
-
- Downloads
-
-
- Download set
- Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.
-
-
- Open download set page
-
-
-
-
- CAs
- | Name | Certificate | Signed certificates |
-
- | {{ ca.Name }} |
- |
-
- {{" "}}
-
- |
-
+
+
Grant Kubernetes API access
+
User: (by default, from the CSR)
+
Group:
+
Certificate signing request (PEM format):
+
+
+
+
+
+ => Get certificate
+
+
+
+
+
Grant download access
+
Generates a signed download set granting access to the selected assets for all {{ hostCount }} hosts in this cluster.
+
Available assets
+
+
+
+ {{" "}}
+
+
+
+
+ Open download set page
+
+
+
+
`
}
diff --git a/html/ui/ClusterCAs-d6eba07c367b6306.js b/html/ui/ClusterCAs-d6eba07c367b6306.js
new file mode 100644
index 0000000..75e520f
--- /dev/null
+++ b/html/ui/ClusterCAs-d6eba07c367b6306.js
@@ -0,0 +1,16 @@
+const ClusterCAs = {
+ components: { GetCopy },
+ props: [ 'cluster', 'token' ],
+ template: `
+ CAs
+ | Name | Certificate | Signed certificates |
+
+ | {{ ca.Name }} |
+ |
+
+ {{" "}}
+
+ |
+
+`
+}
diff --git a/html/ui/index.html b/html/ui/index.html
index 2b5b9c7..9fa80f5 100644
--- a/html/ui/index.html
+++ b/html/ui/index.html
@@ -13,7 +13,9 @@
-
+
+
+
diff --git a/html/ui/style.css b/html/ui/style.css
index 98af93b..664d9a1 100644
--- a/html/ui/style.css
+++ b/html/ui/style.css
@@ -195,3 +195,19 @@ header .utils > * {
}
.copy { font-size: small; }
+
+label.radio {
+ display: inline-block;
+ margin-right: 1ex;
+ margin-bottom: 1ex;
+ padding: 0.5ex;
+ border: 1pt solid;
+ border-radius: 1ex;
+ cursor: pointer;
+}
+label.radio:has(input:checked) {
+ color: var(--link);
+}
+label.radio input {
+ display: none;
+}
diff --git a/ui/index.html b/ui/index.html
index cd93e2e..dad1bf0 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -13,6 +13,8 @@
+
+
diff --git a/ui/js/Cluster.js b/ui/js/Cluster.js
index 827a01f..aa3badf 100644
--- a/ui/js/Cluster.js
+++ b/ui/js/Cluster.js
@@ -1,130 +1,8 @@
const Cluster = {
- components: { Downloads, GetCopy },
+ components: { ClusterAccess, ClusterCAs, Downloads, GetCopy },
props: [ 'cluster', 'token', 'state' ],
- data() {
- return {
- signReqValidity: "1d",
- sshSignReq: {
- PubKey: "",
- Principal: "root",
- },
- sshUserCert: null,
- kubeSignReq: {
- CSR: "",
- User: "",
- Group: "system:masters",
- },
- kubeUserCert: null,
- downloadSet: null,
- };
- },
- methods: {
- sshCASign() {
- event.preventDefault();
- fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, {
- method: 'POST',
- body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }),
- headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
- }).then((resp) => {
- if (resp.ok) {
- resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
- } else {
- resp.json().then((resp) => alert('failed to sign: '+resp.message))
- }
- })
- .catch((e) => { alert('failed to sign: '+e); })
- },
- kubeCASign() {
- event.preventDefault();
- fetch(`/clusters/${this.cluster.Name}/kube/sign`, {
- method: 'POST',
- body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
- headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
- }).then((resp) => {
- if (resp.ok) {
- resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
- } else {
- resp.json().then((resp) => alert('failed to sign: '+resp.message))
- }
- })
- .catch((e) => { alert('failed to sign: '+e); })
- },
- readFile(e, onload) {
- const file = e.target.files[0];
- if (!file) { return; }
- const reader = new FileReader();
- reader.onload = () => { onload(reader.result) };
- reader.onerror = () => { alert("error reading file"); };
- reader.readAsText(file);
- },
- loadPubKey(e) {
- this.readFile(e, (v) => {
- this.sshSignReq.PubKey = v;
- });
- },
- loadCSR(e) {
- this.readFile(e, (v) => {
- this.kubeSignReq.CSR = v;
- });
- },
- generateDownloadSet() {
- event.preventDefault()
-
- const hosts = (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name)
- const items = hosts.map(h => ({
- Kind: "host",
- Name: h.Name,
- Assets: ["kernel", "initrd", "uki", "bootstrap.tar", "boot.img.gz", "boot.img", "boot.qcow2", "boot.iso", "boot.tar", "bootstrap-config", "config", "config.json", "ipxe"],
- }))
-
- fetch('/sign-download-set', {
- method: 'POST',
- body: JSON.stringify({Expiry: this.signReqValidity, Items: items}),
- headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
- }).then((resp) => {
- if (resp.ok) {
- resp.json().then((set) => { this.downloadSet = set })
- } else {
- resp.json().then((resp) => alert('failed to generate: '+resp.message))
- }
- }).catch((e) => { alert('failed to generate: '+e) })
- },
- },
template: `
- Access
-
- Allow cluster access from a public key
-
- Grant SSH access
-
- Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.
- User:
- Public key (OpenSSH format):
-
-
-
-
-
-
- => Get certificate
-
-
-
- Grant Kubernetes API access
-
- Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.
- User: (by default, from the CSR)
- Group:
- Certificate signing request (PEM format):
-
-
-
-
-
-
- => Get certificate
-
-
+
Tokens
@@ -139,25 +17,6 @@ const Cluster = {
Downloads
- Download set
- Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.
-
-
- Open download set page
-
-
-
-
- CAs
- | Name | Certificate | Signed certificates |
-
- | {{ ca.Name }} |
- |
-
- {{" "}}
-
- |
-
-
+
`
}
diff --git a/ui/js/ClusterAccess.js b/ui/js/ClusterAccess.js
new file mode 100644
index 0000000..0544790
--- /dev/null
+++ b/ui/js/ClusterAccess.js
@@ -0,0 +1,178 @@
+const ClusterAccess = {
+ props: [ 'cluster', 'token', 'state' ],
+ data() {
+ return {
+ accessType: "ssh",
+ signReqValidity: "1d",
+ sshSignReq: {
+ PubKey: "",
+ Principal: "root",
+ },
+ sshUserCert: null,
+ kubeSignReq: {
+ CSR: "",
+ User: "",
+ Group: "system:masters",
+ },
+ kubeUserCert: null,
+ downloadSet: null,
+ selectedAssets: {},
+ };
+ },
+ computed: {
+ availableAssets() {
+ return [
+ "kernel",
+ "initrd",
+ "uki",
+ "bootstrap.tar",
+ "boot.img.gz",
+ "boot.img",
+ "boot.qcow2",
+ "boot.vmdk",
+ "boot.iso",
+ "boot.tar",
+ "bootstrap-config",
+ "config",
+ "config.json",
+ "ipxe",
+ ]
+ },
+ selectedAssetList() {
+ return this.availableAssets.filter(a => this.selectedAssets[a])
+ },
+ hostCount() {
+ return (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name).length
+ },
+ },
+ methods: {
+ sshCASign() {
+ event.preventDefault();
+ fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, {
+ method: 'POST',
+ body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }),
+ headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
+ }).then((resp) => {
+ if (resp.ok) {
+ resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
+ } else {
+ resp.json().then((resp) => alert('failed to sign: '+resp.message))
+ }
+ })
+ .catch((e) => { alert('failed to sign: '+e); })
+ },
+ kubeCASign() {
+ event.preventDefault();
+ fetch(`/clusters/${this.cluster.Name}/kube/sign`, {
+ method: 'POST',
+ body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
+ headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
+ }).then((resp) => {
+ if (resp.ok) {
+ resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
+ } else {
+ resp.json().then((resp) => alert('failed to sign: '+resp.message))
+ }
+ })
+ .catch((e) => { alert('failed to sign: '+e); })
+ },
+ generateDownloadSet() {
+ event.preventDefault()
+
+ const hosts = this.hostCount ? (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name) : []
+ const items = hosts.map(h => ({
+ Kind: "host",
+ Name: h.Name,
+ Assets: this.selectedAssetList,
+ }))
+
+ fetch('/sign-download-set', {
+ method: 'POST',
+ body: JSON.stringify({Expiry: this.signReqValidity, Items: items}),
+ headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
+ }).then((resp) => {
+ if (resp.ok) {
+ resp.json().then((set) => { this.downloadSet = set })
+ } else {
+ resp.json().then((resp) => alert('failed to generate: '+resp.message))
+ }
+ }).catch((e) => { alert('failed to generate: '+e) })
+ },
+ readFile(e, onload) {
+ const file = e.target.files[0];
+ if (!file) { return; }
+ const reader = new FileReader();
+ reader.onload = () => { onload(reader.result) };
+ reader.onerror = () => { alert("error reading file"); };
+ reader.readAsText(file);
+ },
+ loadPubKey(e) {
+ this.readFile(e, (v) => {
+ this.sshSignReq.PubKey = v;
+ });
+ },
+ loadCSR(e) {
+ this.readFile(e, (v) => {
+ this.kubeSignReq.CSR = v;
+ });
+ },
+ },
+ template: `
+ Access
+
+ Grant access to:
+
+
+
+
+
+ Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.
+
+
+
Grant SSH access
+
User:
+
Public key (OpenSSH format):
+
+
+
+
+
+ => Get certificate
+
+
+
+
+
+
Grant Kubernetes API access
+
User: (by default, from the CSR)
+
Group:
+
Certificate signing request (PEM format):
+
+
+
+
+
+ => Get certificate
+
+
+
+
+
+
Grant download access
+
Generates a signed download set granting access to the selected assets for all {{ hostCount }} hosts in this cluster.
+
Available assets
+
+
+
+ {{" "}}
+
+
+
+
+ Open download set page
+
+
+
+
+`
+}
diff --git a/ui/js/ClusterCAs.js b/ui/js/ClusterCAs.js
new file mode 100644
index 0000000..75e520f
--- /dev/null
+++ b/ui/js/ClusterCAs.js
@@ -0,0 +1,16 @@
+const ClusterCAs = {
+ components: { GetCopy },
+ props: [ 'cluster', 'token' ],
+ template: `
+ CAs
+ | Name | Certificate | Signed certificates |
+
+ | {{ ca.Name }} |
+ |
+
+ {{" "}}
+
+ |
+
+`
+}
diff --git a/ui/js/ClusterDownloadSet.js b/ui/js/ClusterDownloadSet.js
new file mode 100644
index 0000000..64af00d
--- /dev/null
+++ b/ui/js/ClusterDownloadSet.js
@@ -0,0 +1,43 @@
+const ClusterDownloadSet = {
+ props: [ 'cluster', 'token', 'state' ],
+ data() {
+ return {
+ signReqValidity: "1d",
+ downloadSet: null,
+ };
+ },
+ methods: {
+ generateDownloadSet() {
+ event.preventDefault()
+
+ const hosts = (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name)
+ const items = hosts.map(h => ({
+ Kind: "host",
+ Name: h.Name,
+ Assets: ["kernel", "initrd", "uki", "bootstrap.tar", "boot.img.gz", "boot.img", "boot.qcow2", "boot.iso", "boot.tar", "bootstrap-config", "config", "config.json", "ipxe"],
+ }))
+
+ fetch('/sign-download-set', {
+ method: 'POST',
+ body: JSON.stringify({Expiry: this.signReqValidity, Items: items}),
+ headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
+ }).then((resp) => {
+ if (resp.ok) {
+ resp.json().then((set) => { this.downloadSet = set })
+ } else {
+ resp.json().then((resp) => alert('failed to generate: '+resp.message))
+ }
+ }).catch((e) => { alert('failed to generate: '+e) })
+ },
+ },
+ template: `
+ Download set
+ Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.
+
+
+ Open download set page
+
+
+
+`
+}
diff --git a/ui/style.css b/ui/style.css
index 98af93b..664d9a1 100644
--- a/ui/style.css
+++ b/ui/style.css
@@ -195,3 +195,19 @@ header .utils > * {
}
.copy { font-size: small; }
+
+label.radio {
+ display: inline-block;
+ margin-right: 1ex;
+ margin-bottom: 1ex;
+ padding: 0.5ex;
+ border: 1pt solid;
+ border-radius: 1ex;
+ cursor: pointer;
+}
+label.radio:has(input:checked) {
+ color: var(--link);
+}
+label.radio input {
+ display: none;
+}