diff --git a/.gitignore b/.gitignore index 899e9d9d40212e5c64dfc36b9ebd3b126cc42e5f..ea07f629381a08768e06e31380d970ce6608fd91 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ config/secrets.yaml .meepctl-repocfg.yaml config/api/ charts/grafana/dashboards/mec-sandbox.json +deploy/pyinfra/.env +deploy/pyinfra/pyinfra-venv/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index c961552845cf2464495296077c57948811db029e..19cbace627970f0fead0a98356de6e28fd396131 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "etsi-mec-sandbox-frontend"] path = etsi-mec-sandbox-frontend url = https://labs.etsi.org/rep/mec/etsi-mec-sandbox-frontend.git - branch = STF678_Task4 + branch = STF_685 diff --git a/charts/mec-services/meep-ams/values-template.yaml b/charts/mec-services/meep-ams/values-template.yaml index d58c0ae621e686806f2915a723aa184f10076fd4..880108d1eabe7d0ff1eedf8d611b288da7e312ad 100644 --- a/charts/mec-services/meep-ams/values-template.yaml +++ b/charts/mec-services/meep-ams/values-template.yaml @@ -64,7 +64,7 @@ ingress: rewrite ^/{{.SandboxName}}/amsi(/|$)(.*)$ /amsi/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-ams&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-ams&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-app-enablement/values-template.yaml b/charts/mec-services/meep-app-enablement/values-template.yaml index bab19c75ef1b55067d8c04b25af3b9317188cad7..2a8881679cb996a50fa9638d1ad8ad699719e780 100644 --- a/charts/mec-services/meep-app-enablement/values-template.yaml +++ b/charts/mec-services/meep-app-enablement/values-template.yaml @@ -88,7 +88,7 @@ ingress: rewrite ^/{{ .SandboxName }}/eees-easdiscovery(/|$)(.*)$ /eees-easdiscovery/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-app-enablement&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-app-enablement&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-dai/values-template.yaml b/charts/mec-services/meep-dai/values-template.yaml index be7fd4c2bea2e6a168e8716320d458a8912c1c91..2fef952412312faf70ed3568e1790fd5d183260d 100644 --- a/charts/mec-services/meep-dai/values-template.yaml +++ b/charts/mec-services/meep-dai/values-template.yaml @@ -98,7 +98,7 @@ ingress: rewrite ^/{{.SandboxName}}/onboarded-demo4(/|$)(.*)$ /$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-dai&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-dai&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-federation/values-template.yaml b/charts/mec-services/meep-federation/values-template.yaml index 76b0f7c17cef34cb76fac08a70365ee020fde733..eeb042fb38cfa5ff0e0ebefe9cc7450fb65a9692 100644 --- a/charts/mec-services/meep-federation/values-template.yaml +++ b/charts/mec-services/meep-federation/values-template.yaml @@ -68,7 +68,7 @@ ingress: rewrite ^/{{.SandboxName}}/fed_enablement(/|$)(.*)$ /fed_enablement/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-federation&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-federation&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-iot/values-template.yaml b/charts/mec-services/meep-iot/values-template.yaml index aa0adf1761ef7860cda616a1a148dc41daa4dede..456c7754fde58c27a24d12b8d0b7fa500a1c43b9 100644 --- a/charts/mec-services/meep-iot/values-template.yaml +++ b/charts/mec-services/meep-iot/values-template.yaml @@ -66,7 +66,7 @@ ingress: rewrite ^/{{.SandboxName}}/iots(/|$)(.*)$ /iots/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-iot&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-iot&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-loc-serv/values-template.yaml b/charts/mec-services/meep-loc-serv/values-template.yaml index 537da15e0847b217656878fdf40ed55be6a7c6c2..8a52892408ef119467456c0da82ed3f65b90eeae 100644 --- a/charts/mec-services/meep-loc-serv/values-template.yaml +++ b/charts/mec-services/meep-loc-serv/values-template.yaml @@ -64,7 +64,7 @@ ingress: rewrite ^/{{.SandboxName}}/location(/|$)(.*)$ /location/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-loc-serv&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-loc-serv&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-rnis/values-template.yaml b/charts/mec-services/meep-rnis/values-template.yaml index 56ce4ebf6a45087351eb1a7da95e832e980aeb55..a50d142d7f3f71ffac155dea313505d48309b2e3 100644 --- a/charts/mec-services/meep-rnis/values-template.yaml +++ b/charts/mec-services/meep-rnis/values-template.yaml @@ -66,7 +66,7 @@ ingress: rewrite ^/{{.SandboxName}}/rni(/|$)(.*)$ /rni/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-rnis&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-rnis&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-sss/values-template.yaml b/charts/mec-services/meep-sss/values-template.yaml index 3ff79e09ae0319092b4c78c9cec3f13e8cc0cb10..1ed773699da52a97af9d16cd216661ab1a3f1655 100644 --- a/charts/mec-services/meep-sss/values-template.yaml +++ b/charts/mec-services/meep-sss/values-template.yaml @@ -74,7 +74,7 @@ ingress: rewrite ^/{{.SandboxName}}/onem2m(/|$)(.*)$ /$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-sss&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-sss&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-tm/values-template.yaml b/charts/mec-services/meep-tm/values-template.yaml index 913f06aa1eb4a0cb9dd6d47f2fe411ec9dc19b83..8103877faa08f35abbc90ac4baa88362c53abfd4 100644 --- a/charts/mec-services/meep-tm/values-template.yaml +++ b/charts/mec-services/meep-tm/values-template.yaml @@ -70,7 +70,7 @@ ingress: rewrite ^/{{.SandboxName}}/mts(/|$)(.*)$ /mts/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-tm&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-tm&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-vis/values-template.yaml b/charts/mec-services/meep-vis/values-template.yaml index cb4708404a1fdf5adae4e940d8823d2845bad2f3..e5847cc64644fb16eb1176521e29d7a5b5aec8a6 100644 --- a/charts/mec-services/meep-vis/values-template.yaml +++ b/charts/mec-services/meep-vis/values-template.yaml @@ -70,7 +70,7 @@ ingress: rewrite ^/{{.SandboxName}}/vis(/|$)(.*)$ /vis/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-vis&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-vis&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/mec-services/meep-wais/values-template.yaml b/charts/mec-services/meep-wais/values-template.yaml index 380002545b5903ff116970e1760790207fae905b..ab5e4b1345a169600b3055316f251ae73a561a18 100644 --- a/charts/mec-services/meep-wais/values-template.yaml +++ b/charts/mec-services/meep-wais/values-template.yaml @@ -66,7 +66,7 @@ ingress: rewrite ^/{{.SandboxName}}/wai(/|$)(.*)$ /wai/$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-wais&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-wais&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/messaging/meep-cloud-mosquitto/values-template.yaml b/charts/messaging/meep-cloud-mosquitto/values-template.yaml index 3e03e99c4897d3a842af8ba39ed9cea46ca7bde4..f32eefeef26097665e30b4aa9489713891c970b2 100644 --- a/charts/messaging/meep-cloud-mosquitto/values-template.yaml +++ b/charts/messaging/meep-cloud-mosquitto/values-template.yaml @@ -65,7 +65,7 @@ ingress: rewrite ^/{{.SandboxName}}/meep-cloud-mosquitto(/|$)(.*)$ /$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-cloud-mosquitto&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-cloud-mosquitto&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/messaging/meep-mosquitto/values-template.yaml b/charts/messaging/meep-mosquitto/values-template.yaml index 96b76e1be7ec60da31ecb854c714a91d3d2bd782..252636a54e22dc107796e645b86de4207cb1dd6c 100644 --- a/charts/messaging/meep-mosquitto/values-template.yaml +++ b/charts/messaging/meep-mosquitto/values-template.yaml @@ -66,7 +66,7 @@ ingress: rewrite ^/{{.SandboxName}}/meep-mosquitto(/|$)(.*)$ /$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-mosquitto&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-mosquitto&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/observability/kube-prometheus-stack/templates/alertmanager/persistentvolume.yaml b/charts/observability/kube-prometheus-stack/templates/alertmanager/persistentvolume.yaml index 958a9e4ad5f803737274fbbfeaa6d111540444b4..0f75595ff6f020b8cd837317dc9facdeb2fbeb55 100755 --- a/charts/observability/kube-prometheus-stack/templates/alertmanager/persistentvolume.yaml +++ b/charts/observability/kube-prometheus-stack/templates/alertmanager/persistentvolume.yaml @@ -3,6 +3,8 @@ kind: PersistentVolume apiVersion: v1 metadata: name: {{ template "kube-prometheus-stack.fullname" . }}-alertmanager + annotations: + "helm.sh/resource-policy": keep labels: app: {{ template "kube-prometheus-stack.name" . }}-alertmanager chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} diff --git a/charts/observability/kube-prometheus-stack/templates/prometheus/persistentvolume.yaml b/charts/observability/kube-prometheus-stack/templates/prometheus/persistentvolume.yaml index 091e87027811424f5391346dbcea7bb862c3f1d0..ed4d60f20c944532a40a67e93f984a050ceaaf25 100755 --- a/charts/observability/kube-prometheus-stack/templates/prometheus/persistentvolume.yaml +++ b/charts/observability/kube-prometheus-stack/templates/prometheus/persistentvolume.yaml @@ -3,6 +3,8 @@ kind: PersistentVolume apiVersion: v1 metadata: name: {{ template "kube-prometheus-stack.fullname" . }}-server + annotations: + "helm.sh/resource-policy": keep labels: app: {{ template "kube-prometheus-stack.name" . }}-server chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} diff --git a/charts/observability/kube-prometheus-stack/values.yaml b/charts/observability/kube-prometheus-stack/values.yaml index fddd4843a6a1a408b7920d9949ec44c9a12d4344..5c5cc9834582be86261cc9f14b053b8485d7bfaa 100644 --- a/charts/observability/kube-prometheus-stack/values.yaml +++ b/charts/observability/kube-prometheus-stack/values.yaml @@ -1523,7 +1523,7 @@ prometheusOperator: patch: enabled: true image: - repository: k8s.gcr.io/ingress-nginx/kube-webhook-certgen + repository: registry.k8s.io/ingress-nginx/kube-webhook-certgen tag: v1.1.1 sha: "" pullPolicy: IfNotPresent diff --git a/charts/observability/thanos/templates/rustfs/hostpath-pv.yaml b/charts/observability/thanos/templates/rustfs/hostpath-pv.yaml index 26b53bb943073ec1292c56c0fcc37f586cd3939c..335479b9603839ac532f472cec4c7545e657eabf 100644 --- a/charts/observability/thanos/templates/rustfs/hostpath-pv.yaml +++ b/charts/observability/thanos/templates/rustfs/hostpath-pv.yaml @@ -8,6 +8,8 @@ apiVersion: v1 kind: PersistentVolume metadata: name: {{ .Release.Name }}-rustfs-data + annotations: + "helm.sh/resource-policy": keep labels: {{- include "thanos.labels" . | nindent 4 }} spec: @@ -24,6 +26,8 @@ apiVersion: v1 kind: PersistentVolume metadata: name: {{ .Release.Name }}-rustfs-logs + annotations: + "helm.sh/resource-policy": keep labels: {{- include "thanos.labels" . | nindent 4 }} spec: diff --git a/charts/onem2m-iot/meep-acme-in-cse/values-template.yaml b/charts/onem2m-iot/meep-acme-in-cse/values-template.yaml index 83d9d0d0ab97c93b839e9bdd0b1ab2502788b02b..619c9e15f8e8135dc991125d8fa67a8c8d315232 100644 --- a/charts/onem2m-iot/meep-acme-in-cse/values-template.yaml +++ b/charts/onem2m-iot/meep-acme-in-cse/values-template.yaml @@ -78,7 +78,7 @@ ingress: rewrite ^/{{.SandboxName}}/meep-acme-in-cse(/|$)(.*)$ /$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-acme-in-cse&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-acme-in-cse&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/onem2m-iot/meep-acme-mn-cse/values-template.yaml b/charts/onem2m-iot/meep-acme-mn-cse/values-template.yaml index 8d959074c45cbfbde6eba889428e9539df1f5b15..ad1ed996df9560c0f3cdd4f3c527d49fe2063ae8 100644 --- a/charts/onem2m-iot/meep-acme-mn-cse/values-template.yaml +++ b/charts/onem2m-iot/meep-acme-mn-cse/values-template.yaml @@ -82,7 +82,7 @@ ingress: rewrite ^/{{.SandboxName}}/meep-acme-mn-cse(/|$)(.*)$ /$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-acme-mn-cse&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-acme-mn-cse&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/onem2m-iot/meep-tinyiot-in-cse/values-template.yaml b/charts/onem2m-iot/meep-tinyiot-in-cse/values-template.yaml index 541137749c9b2b7e825c6d85d2b2a9a3fce7a463..e2802beca4420d8ea8a75eb6572595af491dc158 100644 --- a/charts/onem2m-iot/meep-tinyiot-in-cse/values-template.yaml +++ b/charts/onem2m-iot/meep-tinyiot-in-cse/values-template.yaml @@ -78,7 +78,7 @@ ingress: rewrite ^/{{.SandboxName}}/meep-tinyiot-in-cse(/|$)(.*)$ /$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-tinyiot-in-cse&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-tinyiot-in-cse&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/onem2m-iot/meep-tinyiot-mn-cse/values-template.yaml b/charts/onem2m-iot/meep-tinyiot-mn-cse/values-template.yaml index 05c8854f55e29e094642c78a7b35a5a165da3e97..c8ff0f549b310678d3d1e513b1f62aa5f1410245 100644 --- a/charts/onem2m-iot/meep-tinyiot-mn-cse/values-template.yaml +++ b/charts/onem2m-iot/meep-tinyiot-mn-cse/values-template.yaml @@ -76,7 +76,7 @@ ingress: rewrite ^/{{.SandboxName}}/meep-tinyiot-mn-cse(/|$)(.*)$ /$2 break; {{- end }} {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-tinyiot-mn-cse&sbox={{.SandboxName}}&mep={{.LocationName}} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-tinyiot-mn-cse&sbox={{.SandboxName}}&mep={{.LocationName}} {{- end }} labels: {} tls: diff --git a/charts/platform-core/meep-gis-engine/values-template.yaml b/charts/platform-core/meep-gis-engine/values-template.yaml index 50204eee7100d9e3c74d2ca84b14465a219f5b5a..995caf20788ebe5b922e12743bca0d6b625ebd63 100644 --- a/charts/platform-core/meep-gis-engine/values-template.yaml +++ b/charts/platform-core/meep-gis-engine/values-template.yaml @@ -38,7 +38,7 @@ ingress: nginx.ingress.kubernetes.io/configuration-snippet: | rewrite ^/{{ .SandboxName }}/gis(/|$)(.*)$ /gis/$2 break; {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-gis-engine&sbox={{ .SandboxName }} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-gis-engine&sbox={{ .SandboxName }} {{- end }} labels: {} tls: diff --git a/charts/platform-core/meep-metrics-engine/values-template.yaml b/charts/platform-core/meep-metrics-engine/values-template.yaml index 4741ab88dcb8841aec368137c327018a34c255fb..0bb293520905469c289ca2855fc57202b92c5c77 100644 --- a/charts/platform-core/meep-metrics-engine/values-template.yaml +++ b/charts/platform-core/meep-metrics-engine/values-template.yaml @@ -39,7 +39,7 @@ ingress: nginx.ingress.kubernetes.io/configuration-snippet: | rewrite ^/{{ .SandboxName }}/metrics(/|$)(.*)$ /metrics/$2 break; {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-metrics-engine&sbox={{ .SandboxName }} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-metrics-engine&sbox={{ .SandboxName }} {{- end }} labels: {} tls: diff --git a/charts/platform-core/meep-mg-manager/values-template.yaml b/charts/platform-core/meep-mg-manager/values-template.yaml index 3196161453830d97c830eaa2ca5531a9d5c373a2..ea254d8cbcd51a6571572238eab2cf11f18bca70 100644 --- a/charts/platform-core/meep-mg-manager/values-template.yaml +++ b/charts/platform-core/meep-mg-manager/values-template.yaml @@ -49,7 +49,7 @@ ingress: nginx.ingress.kubernetes.io/configuration-snippet: | rewrite ^/{{ .SandboxName }}/mgm(/|$)(.*)$ /mgm/$2 break; {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-mg-manager&sbox={{ .SandboxName }} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-mg-manager&sbox={{ .SandboxName }} {{- end }} labels: {} tls: diff --git a/charts/platform-core/meep-platform-ctrl/templates/deployment.yaml b/charts/platform-core/meep-platform-ctrl/templates/deployment.yaml index 85b6b88c9a2de178d3cb5b4bd1c5a8347a305c8f..156e40549d73dbfaedb60e86087830ae6acbf799 100644 --- a/charts/platform-core/meep-platform-ctrl/templates/deployment.yaml +++ b/charts/platform-core/meep-platform-ctrl/templates/deployment.yaml @@ -76,6 +76,9 @@ spec: {{ toYaml .Values.affinity | indent 8 }} {{- end }} initContainers: + - name: wait-for-couchdb + image: busybox + command: ['sh', '-c', 'until nc -z meep-couchdb-svc-couchdb 5984; do echo waiting for db; sleep 2; done;'] {{- range $value := .Values.deployment.dependencies.system }} - name: init-system-{{ $value }} image: busybox:1.28 diff --git a/charts/platform-core/meep-sandbox-ctrl/values-template.yaml b/charts/platform-core/meep-sandbox-ctrl/values-template.yaml index 7debfafe716771596ab02e6298d7a03e85f4df3b..a675db96a4c93a5e2d8ff129ca0eebdba15bec95 100644 --- a/charts/platform-core/meep-sandbox-ctrl/values-template.yaml +++ b/charts/platform-core/meep-sandbox-ctrl/values-template.yaml @@ -33,6 +33,7 @@ image: MEEP_SANDBOX_NAME: {{ .SandboxName }} MEEP_SVC_PATH: /sandbox-ctrl/v1 MEEP_HOST_URL: {{ .HostUrl }} + MEEP_HTTPS_ONLY: {{ .HttpsOnly }} service: type: ClusterIP @@ -56,7 +57,7 @@ ingress: rewrite ^/{{ .SandboxName }}/alt/api(/|$)(.*)$ /alt/api/$2 break; rewrite ^/{{ .SandboxName }}/sandbox-ctrl(/|$)(.*)$ /sandbox-ctrl/$2 break; {{- if .AuthEnabled }} - nginx.ingress.kubernetes.io/auth-url: https://$http_host/auth/v1/authenticate?svc=meep-sandbox-ctrl&sbox={{ .SandboxName }} + nginx.ingress.kubernetes.io/auth-url: {{ if .HttpsOnly }}https{{ else }}http{{ end }}://$http_host/auth/v1/authenticate?svc=meep-sandbox-ctrl&sbox={{ .SandboxName }} {{- end }} labels: {} tls: diff --git a/charts/platform-core/meep-webhook/templates/configmap.yaml b/charts/platform-core/meep-webhook/templates/configmap.yaml index 60ebb09d2dec642ac9a46fd50bacc29fc336bbec..7903fe88fcb47c53fb6b2f622d137d9c1198d506 100644 --- a/charts/platform-core/meep-webhook/templates/configmap.yaml +++ b/charts/platform-core/meep-webhook/templates/configmap.yaml @@ -16,4 +16,6 @@ data: - name: init-{{ .Values.sidecar.dependency }} image: busybox:1.28 imagePullPolicy: IfNotPresent - command: ['sh', '-c', 'until nslookup {{ .Values.sidecar.dependency }}.kube-system ; do echo waiting for {{ .Values.sidecar.dependency }}; sleep 0.25; done;'] + securityContext: + privileged: true + command: ['sh', '-c', 'sysctl -w net.ipv4.ip_forward=1 || true; until nslookup {{ .Values.sidecar.dependency }}.kube-system ; do echo waiting for {{ .Values.sidecar.dependency }}; sleep 0.25; done;'] diff --git a/config/objstore-thanos-archive.yaml b/config/objstore-thanos-archive.yaml index 982fe0a9a1d5a91a7ce1327be3c5107d92f99b53..297e5e078343996e99c86fb01655e6f040d2d49d 100644 --- a/config/objstore-thanos-archive.yaml +++ b/config/objstore-thanos-archive.yaml @@ -1,7 +1,7 @@ type: s3 config: bucket: thanos-archive - endpoint: my-fqdn-or-ip + endpoint: metrics.try-mec.etsi.org access_key: my-access-key secret_key: my-secret-key insecure: false @@ -10,3 +10,4 @@ config: idle_conn_timeout: 1m30s response_header_timeout: 2m insecure_skip_verify: false + diff --git a/config/objstore-thanos.yaml b/config/objstore-thanos.yaml index 27c8b1736661745ca81c7a56256444aba58c8fb0..57d96f87e66ce9f053dafbbd1e97862485174d07 100644 --- a/config/objstore-thanos.yaml +++ b/config/objstore-thanos.yaml @@ -1,7 +1,7 @@ type: s3 config: bucket: thanos - endpoint: my-fqdn-or-ip + endpoint: metrics.try-mec.etsi.org access_key: my-access-key secret_key: my-secret-key insecure: false diff --git a/config/secrets.yaml b/config/secrets.yaml index 7cf80d720794262d2d9b7e0d41006855f2260234..5c6bc695bb70cb9f683f002d31f001f9e984fd2f 100644 --- a/config/secrets.yaml +++ b/config/secrets.yaml @@ -1,11 +1,14 @@ meep-session: - encryption-key: "my-secret-key" + encryption-key: "my-secret-encryption-key" meep-oauth-github: - client-id: "my-github-client-id" - secret: "my-github-secret" + client-id: "Ov23liZqkabhEidZnJgp" + secret: "674e60b667956bcdca3fbbc99002ed1f2d50d9f2" meep-oauth-gitlab: - client-id: "my-gitlab-client-id" - secret: "my-gitlab-secret" + client-id: "4accd71e13764749c16afee22b36f10422c146922efa2beea786842773f97b24" + secret: "gloas-1b312471470fc8e27cf075c9aa8464ef66f7bc6ed46804ddcc9b5b9fe66c6271" +meep-minio-objstore-config: + accesskey: "my-access-key" + secretkey: "my-secret-key" diff --git a/playbooks/.ansible-lint b/deploy/ansible/.ansible-lint similarity index 100% rename from playbooks/.ansible-lint rename to deploy/ansible/.ansible-lint diff --git a/playbooks/README.md b/deploy/ansible/README.md similarity index 92% rename from playbooks/README.md rename to deploy/ansible/README.md index cc814455050a4e2146d9090973b61d0142711d7a..c0bd360ddb3df23c06b9ccdbcb76a6ff275dd064 100644 --- a/playbooks/README.md +++ b/deploy/ansible/README.md @@ -24,10 +24,10 @@ Before running the playbooks, ensure: Before running any playbooks, set up the Ansible environment: ```bash -chmod +x ~/etsi-mec-sandbox/playbooks/setup_ansible_env.sh -cd ~/etsi-mec-sandbox/playbooks +chmod +x ~/etsi-mec-sandbox/deploy/ansible/setup_ansible_env.sh +cd ~/etsi-mec-sandbox/deploy/ansible ./setup_ansible_env.sh -source ~/etsi-mec-sandbox/playbooks/ansible-venv/bin/activate +source ~/etsi-mec-sandbox/deploy/ansible/ansible-venv/bin/activate ``` --- @@ -36,10 +36,10 @@ source ~/etsi-mec-sandbox/playbooks/ansible-venv/bin/activate ```bash # Activate virtual environment -source ~/etsi-mec-sandbox/playbooks/ansible-venv/bin/activate +source ~/etsi-mec-sandbox/deploy/ansible/ansible-venv/bin/activate # Run the playbook -cd ~/etsi-mec-sandbox/playbooks +cd ~/etsi-mec-sandbox/deploy/ansible ansible-playbook -i inventories/dev/hosts.ini site.yml ``` @@ -55,7 +55,7 @@ You will be prompted for: ## Folder Structure ``` -playbooks/ +deploy/ansible/ ├── setup_ansible_env.sh # Environment setup script (run first!) ├── site.yml # Main playbook entrypoint ├── ansible.cfg # Ansible configuration diff --git a/playbooks/RUNBOOK.md b/deploy/ansible/RUNBOOK.md similarity index 95% rename from playbooks/RUNBOOK.md rename to deploy/ansible/RUNBOOK.md index 28084ac9dc99c973be269a32d9652a79273a8a82..fa4906e94afa0d19b81273bf473702e663e0623e 100644 --- a/playbooks/RUNBOOK.md +++ b/deploy/ansible/RUNBOOK.md @@ -24,16 +24,16 @@ Before running any playbooks, you must set up the Ansible environment: ```bash # Make the setup script executable -chmod +x ~/etsi-mec-sandbox/playbooks/setup_ansible_env.sh +chmod +x ~/etsi-mec-sandbox/deploy/ansible/setup_ansible_env.sh # Navigate to the playbooks directory -cd ~/etsi-mec-sandbox/playbooks +cd ~/etsi-mec-sandbox/deploy/ansible # Run the setup script ./setup_ansible_env.sh # Activate the virtual environment -source ~/etsi-mec-sandbox/playbooks/ansible-venv/bin/activate +source ~/etsi-mec-sandbox/deploy/ansible/ansible-venv/bin/activate ``` The setup script: @@ -74,16 +74,16 @@ ansible_become_method=sudo ### Step 1: Setup Environment (if not done) ```bash -chmod +x ~/etsi-mec-sandbox/playbooks/setup_ansible_env.sh -cd ~/etsi-mec-sandbox/playbooks +chmod +x ~/etsi-mec-sandbox/deploy/ansible/setup_ansible_env.sh +cd ~/etsi-mec-sandbox/deploy/ansible ./setup_ansible_env.sh -source ~/etsi-mec-sandbox/playbooks/ansible-venv/bin/activate +source ~/etsi-mec-sandbox/deploy/ansible/ansible-venv/bin/activate ``` ### Step 2: Run the Playbook ```bash -cd ~/etsi-mec-sandbox/playbooks +cd ~/etsi-mec-sandbox/deploy/ansible ansible-playbook -i inventories/dev/hosts.ini site.yml ``` @@ -239,7 +239,7 @@ ansible-playbook -i inventories/dev/hosts.ini site.yml -e "install_dev_env=false ### Virtual Environment Not Activated If you see "ansible: command not found", activate the virtual environment: ```bash -source ~/etsi-mec-sandbox/playbooks/ansible-venv/bin/activate +source ~/etsi-mec-sandbox/deploy/ansible/ansible-venv/bin/activate ``` ### Thanos/Prometheus Deployment Failures @@ -268,7 +268,7 @@ newgrp docker ### Kubernetes Collection Errors If you see errors about `kubernetes.core.k8s`, ensure collections are installed: ```bash -source ~/etsi-mec-sandbox/playbooks/ansible-venv/bin/activate +source ~/etsi-mec-sandbox/deploy/ansible/ansible-venv/bin/activate ansible-galaxy collection install -r collections/requirements.yml ``` diff --git a/playbooks/ansible.cfg b/deploy/ansible/ansible.cfg similarity index 100% rename from playbooks/ansible.cfg rename to deploy/ansible/ansible.cfg diff --git a/playbooks/collections/requirements.yml b/deploy/ansible/collections/requirements.yml similarity index 100% rename from playbooks/collections/requirements.yml rename to deploy/ansible/collections/requirements.yml diff --git a/playbooks/inventories/dev/group_vars/all.yml b/deploy/ansible/inventories/dev/group_vars/all.yml similarity index 100% rename from playbooks/inventories/dev/group_vars/all.yml rename to deploy/ansible/inventories/dev/group_vars/all.yml diff --git a/playbooks/inventories/dev/hosts.ini b/deploy/ansible/inventories/dev/hosts.ini similarity index 100% rename from playbooks/inventories/dev/hosts.ini rename to deploy/ansible/inventories/dev/hosts.ini diff --git a/playbooks/roles/cni_calico/tasks/main.yml b/deploy/ansible/roles/cni_calico/tasks/main.yml similarity index 100% rename from playbooks/roles/cni_calico/tasks/main.yml rename to deploy/ansible/roles/cni_calico/tasks/main.yml diff --git a/playbooks/roles/common/tasks/main.yml b/deploy/ansible/roles/common/tasks/main.yml similarity index 100% rename from playbooks/roles/common/tasks/main.yml rename to deploy/ansible/roles/common/tasks/main.yml diff --git a/playbooks/roles/containerd/handlers/main.yml b/deploy/ansible/roles/containerd/handlers/main.yml similarity index 100% rename from playbooks/roles/containerd/handlers/main.yml rename to deploy/ansible/roles/containerd/handlers/main.yml diff --git a/playbooks/roles/containerd/tasks/main.yml b/deploy/ansible/roles/containerd/tasks/main.yml similarity index 100% rename from playbooks/roles/containerd/tasks/main.yml rename to deploy/ansible/roles/containerd/tasks/main.yml diff --git a/playbooks/roles/dev_env/golang/tasks/install.yml b/deploy/ansible/roles/dev_env/golang/tasks/install.yml similarity index 100% rename from playbooks/roles/dev_env/golang/tasks/install.yml rename to deploy/ansible/roles/dev_env/golang/tasks/install.yml diff --git a/playbooks/roles/dev_env/golang/tasks/main.yml b/deploy/ansible/roles/dev_env/golang/tasks/main.yml similarity index 100% rename from playbooks/roles/dev_env/golang/tasks/main.yml rename to deploy/ansible/roles/dev_env/golang/tasks/main.yml diff --git a/playbooks/roles/dev_env/node/files/install_nvm.sh b/deploy/ansible/roles/dev_env/node/files/install_nvm.sh similarity index 100% rename from playbooks/roles/dev_env/node/files/install_nvm.sh rename to deploy/ansible/roles/dev_env/node/files/install_nvm.sh diff --git a/playbooks/roles/dev_env/node/tasks/install.yml b/deploy/ansible/roles/dev_env/node/tasks/install.yml similarity index 100% rename from playbooks/roles/dev_env/node/tasks/install.yml rename to deploy/ansible/roles/dev_env/node/tasks/install.yml diff --git a/playbooks/roles/dev_env/node/tasks/main.yml b/deploy/ansible/roles/dev_env/node/tasks/main.yml similarity index 100% rename from playbooks/roles/dev_env/node/tasks/main.yml rename to deploy/ansible/roles/dev_env/node/tasks/main.yml diff --git a/playbooks/roles/docker/files/daemon.json b/deploy/ansible/roles/docker/files/daemon.json similarity index 100% rename from playbooks/roles/docker/files/daemon.json rename to deploy/ansible/roles/docker/files/daemon.json diff --git a/playbooks/roles/docker/handlers/main.yml b/deploy/ansible/roles/docker/handlers/main.yml similarity index 100% rename from playbooks/roles/docker/handlers/main.yml rename to deploy/ansible/roles/docker/handlers/main.yml diff --git a/playbooks/roles/docker/tasks/install.yml b/deploy/ansible/roles/docker/tasks/install.yml similarity index 100% rename from playbooks/roles/docker/tasks/install.yml rename to deploy/ansible/roles/docker/tasks/install.yml diff --git a/playbooks/roles/docker/tasks/main.yml b/deploy/ansible/roles/docker/tasks/main.yml similarity index 100% rename from playbooks/roles/docker/tasks/main.yml rename to deploy/ansible/roles/docker/tasks/main.yml diff --git a/playbooks/roles/docker/tasks/repo.yml b/deploy/ansible/roles/docker/tasks/repo.yml similarity index 100% rename from playbooks/roles/docker/tasks/repo.yml rename to deploy/ansible/roles/docker/tasks/repo.yml diff --git a/playbooks/roles/helm/tasks/main.yml b/deploy/ansible/roles/helm/tasks/main.yml similarity index 100% rename from playbooks/roles/helm/tasks/main.yml rename to deploy/ansible/roles/helm/tasks/main.yml diff --git a/playbooks/roles/kernel/handlers/main.yml b/deploy/ansible/roles/kernel/handlers/main.yml similarity index 100% rename from playbooks/roles/kernel/handlers/main.yml rename to deploy/ansible/roles/kernel/handlers/main.yml diff --git a/playbooks/roles/kernel/tasks/main.yml b/deploy/ansible/roles/kernel/tasks/main.yml similarity index 100% rename from playbooks/roles/kernel/tasks/main.yml rename to deploy/ansible/roles/kernel/tasks/main.yml diff --git a/playbooks/roles/kubernetes/common/tasks/install.yml b/deploy/ansible/roles/kubernetes/common/tasks/install.yml similarity index 100% rename from playbooks/roles/kubernetes/common/tasks/install.yml rename to deploy/ansible/roles/kubernetes/common/tasks/install.yml diff --git a/playbooks/roles/kubernetes/common/tasks/main.yml b/deploy/ansible/roles/kubernetes/common/tasks/main.yml similarity index 100% rename from playbooks/roles/kubernetes/common/tasks/main.yml rename to deploy/ansible/roles/kubernetes/common/tasks/main.yml diff --git a/playbooks/roles/kubernetes/master/handlers/main.yml b/deploy/ansible/roles/kubernetes/master/handlers/main.yml similarity index 100% rename from playbooks/roles/kubernetes/master/handlers/main.yml rename to deploy/ansible/roles/kubernetes/master/handlers/main.yml diff --git a/playbooks/roles/kubernetes/master/meta/main.yml b/deploy/ansible/roles/kubernetes/master/meta/main.yml similarity index 100% rename from playbooks/roles/kubernetes/master/meta/main.yml rename to deploy/ansible/roles/kubernetes/master/meta/main.yml diff --git a/playbooks/roles/kubernetes/master/tasks/main.yml b/deploy/ansible/roles/kubernetes/master/tasks/main.yml similarity index 100% rename from playbooks/roles/kubernetes/master/tasks/main.yml rename to deploy/ansible/roles/kubernetes/master/tasks/main.yml diff --git a/playbooks/roles/kubernetes/worker/tasks/main.yml b/deploy/ansible/roles/kubernetes/worker/tasks/main.yml similarity index 100% rename from playbooks/roles/kubernetes/worker/tasks/main.yml rename to deploy/ansible/roles/kubernetes/worker/tasks/main.yml diff --git a/playbooks/roles/mec_sandbox/mec_config/tasks/main.yml b/deploy/ansible/roles/mec_sandbox/mec_config/tasks/main.yml similarity index 100% rename from playbooks/roles/mec_sandbox/mec_config/tasks/main.yml rename to deploy/ansible/roles/mec_sandbox/mec_config/tasks/main.yml diff --git a/playbooks/roles/mec_sandbox/mec_deploy/tasks/main.yml b/deploy/ansible/roles/mec_sandbox/mec_deploy/tasks/main.yml similarity index 100% rename from playbooks/roles/mec_sandbox/mec_deploy/tasks/main.yml rename to deploy/ansible/roles/mec_sandbox/mec_deploy/tasks/main.yml diff --git a/playbooks/setup_ansible_env.sh b/deploy/ansible/setup_ansible_env.sh old mode 100755 new mode 100644 similarity index 86% rename from playbooks/setup_ansible_env.sh rename to deploy/ansible/setup_ansible_env.sh index 9af00e230ca908df98b7644369b67ffa98800678..f4488c5031e2937941b4a3991a4f873fb6dfd7c0 --- a/playbooks/setup_ansible_env.sh +++ b/deploy/ansible/setup_ansible_env.sh @@ -2,13 +2,13 @@ set -euo pipefail -PLAYBOOK_DIR="$HOME/etsi-mec-sandbox/playbooks" +PLAYBOOK_DIR="$HOME/etsi-mec-sandbox/deploy/ansible" SANDBOX_DIR="$HOME/etsi-mec-sandbox" VENV_NAME="ansible-venv" VENV_PATH="$PLAYBOOK_DIR/$VENV_NAME" COLLECTION_REQ="$PLAYBOOK_DIR/collections/requirements.yml" GITIGNORE_FILE="$SANDBOX_DIR/.gitignore" -GITIGNORE_ENTRY="playbooks/$VENV_NAME/" +GITIGNORE_ENTRY="deploy/ansible/$VENV_NAME/" error() { echo "ERROR: $1" >&2; exit 1; } command_exists() { command -v "$1" >/dev/null 2>&1; } @@ -56,9 +56,9 @@ install_ansible_collections() { update_gitignore() { [[ -f "$GITIGNORE_FILE" ]] || touch "$GITIGNORE_FILE" - if ! grep -q "^playbooks/$VENV_NAME" "$GITIGNORE_FILE"; then - echo "playbooks/$VENV_NAME/" >> "$GITIGNORE_FILE" - echo "Added playbooks/$VENV_NAME/ to .gitignore" + if ! grep -q "^deploy/ansible/$VENV_NAME" "$GITIGNORE_FILE"; then + echo "deploy/ansible/$VENV_NAME/" >> "$GITIGNORE_FILE" + echo "Added deploy/ansible/$VENV_NAME/ to .gitignore" else echo ".gitignore already contains entry for $VENV_NAME" fi diff --git a/playbooks/site.yml b/deploy/ansible/site.yml similarity index 100% rename from playbooks/site.yml rename to deploy/ansible/site.yml diff --git a/deploy/pyinfra/.env.example b/deploy/pyinfra/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..b3367d5b44da56a54a661bcc9b5c68790c35b3ed --- /dev/null +++ b/deploy/pyinfra/.env.example @@ -0,0 +1,36 @@ +# Pyinfra MEC Sandbox Environment Variables +# Copy this file to '.env' and fill in your actual values. + +# ---------------------------------------------------- +# Inventory Configuration +# ---------------------------------------------------- +# Target hosts for Kubernetes cluster. +# NOTE: Exactly 1 master node is supported for K8S_MASTERS (control plane). +# For local deployment, use "localhost". +# For remote deployment, specify target hosts in mandatory @ format +# (e.g., K8S_MASTERS="ubuntu@192.168.1.10" and K8S_WORKERS="admin@192.168.1.11,ubuntu@192.168.1.12"). +K8S_MASTERS="localhost" +K8S_WORKERS="" + +# ---------------------------------------------------- + +# Sandbox Configuration +# ---------------------------------------------------- +# The IP or domain name where the MEC Sandbox will be accessible +# e.g., 192.168.1.100 or mec.example.com +MEC_HOST_ADDRESS="" + +# The protocol to use for the ingress (http or https) +MEC_PROTOCOL="https" + +# ---------------------------------------------------- +# GitHub OAuth Secrets +# ---------------------------------------------------- +GITHUB_CLIENT_ID="your-github-client-id" +GITHUB_CLIENT_SECRET="your-github-client-secret" + +# ---------------------------------------------------- +# GitLab OAuth Secrets +# ---------------------------------------------------- +GITLAB_CLIENT_ID="your-gitlab-client-id" +GITLAB_CLIENT_SECRET="your-gitlab-client-secret" diff --git a/deploy/pyinfra/README.md b/deploy/pyinfra/README.md new file mode 100644 index 0000000000000000000000000000000000000000..221585b1f825c596e88552eb92ee3691df260ddb --- /dev/null +++ b/deploy/pyinfra/README.md @@ -0,0 +1,158 @@ +# ETSI MEC Sandbox Automated Deployment Guide + +This guide walks you through deploying the **ETSI MEC Sandbox** using our automated, Python-based infrastructure-as-code ([PyInfra](https://pyinfra.com/)) framework. + +The framework automates the entire provisioning lifecycle—including kernel tuning, Docker/Containerd runtime installation, Kubernetes (`kubeadm`) cluster initialization, development tools (Go, Node.js, NVM), and compiling and running the MEC Sandbox microservices (`meepctl`). + +--- + +## Automated Deployment Workflow + +The diagram below illustrates the automated deployment workflow from setup and `.env` configuration to validation and execution: + +```mermaid +flowchart LR + subgraph SETUP ["1. Setup & Config"] + direction TB + S1["run ./setup.sh"] --> S2["source pyinfra-venv/bin/activate"] + S2 --> S3["edit .env (K8S_MASTERS, MEC_HOST, OAuth)"] + end + + subgraph DEPLOY ["2. Execute Deployment"] + direction TB + D1["pyinfra inventory.py deploy.py"] --> D2{"OAuth & Config Valid?"} + D2 -- "No" --> ERR["Halt with validation error"] + D2 -- "Yes" --> D3["Prompt for Sudo Passwords (in-memory)"] + end + + subgraph ENGINE ["3. Automated Provisioning"] + direction TB + E1["System & Kubernetes (kubeadm)"] --> E2["Build & Deploy Microservices"] + E2 --> E3(["MEC Sandbox Live"]) + end + + S3 --> D1 + D3 --> E1 + + style S1 fill:#1f6feb,stroke:#388bfd,color:#ffffff + style D1 fill:#238636,stroke:#2ea043,color:#ffffff + style E3 fill:#8957e5,stroke:#a371f7,color:#ffffff + style ERR fill:#da3633,stroke:#f85149,color:#ffffff +``` + +--- + +## Prerequisites + +Before deploying, ensure your target machine(s) meet the following requirements: +- **Operating System:** Ubuntu 20.04/22.04 LTS (or Debian-compatible Linux). +- **Python:** Python 3.8+ installed on the machine running this deployment. +- **Privileges:** Sudo (root) access on the target deployment machines. +- **Network:** Outbound internet access to download required containers, packages, and binaries. + +--- + +## Quick Start (3 Steps) + +### Step 1: Initialize the Deployment Environment +Run the automated setup script to verify Python 3, create an isolated virtual environment (`pyinfra-venv`), and install all required deployment dependencies: + +```bash +cd ~/etsi-mec-sandbox/deploy/pyinfra +./setup.sh +``` + +### Step 2: Configure Your Environment (`.env`) +The first time you run `./setup.sh`, it generates a `.env` configuration file from `.env.example` and pauses so you can enter your settings. + +Open `.env` in your text editor and configure the following required fields: +- **`K8S_MASTERS`:** Mandatory target host for the Kubernetes control plane (exactly 1 master node is supported; e.g., `localhost` for local deployments, or `ubuntu@192.168.1.10` for remote servers). +- **`K8S_WORKERS`:** Optional comma-separated list of worker node IPs/hostnames. Leave blank (`""`) for single-machine deployments. +- **`MEC_HOST_ADDRESS`:** The routable IP address or domain name where the MEC Sandbox frontend will be accessible (e.g., `127.0.0.1`, `192.168.1.100`, or `mec.example.com`). +- **OAuth Provider Credentials:** Provide valid OAuth secrets for **GitHub** (`GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`), **GitLab**, or both. Unconfigured providers are automatically disabled in the platform configuration. + +> ! IMPORTANT
+> DO NOT set both K8S_MASTERS and K8S_WORKERS to localhost. +> A single machine cannot act as both an independent Kubernetes master and worker node. For an all-in-one sandbox on your local machine, set `K8S_MASTERS="localhost"` and leave `K8S_WORKERS=""`. + +### Step 3: Run the Deployment +Activate the virtual environment and launch the deployment: + +```bash +source pyinfra-venv/bin/activate +pyinfra inventory.py deploy.py +``` + +--- + +## Authentication & Sudo Passwords + +For security, sudo passwords are NEVER stored in config files or environment variables. + +When you launch `pyinfra inventory.py deploy.py`: +1. **Prompted Once at Startup:** PyInfra will prompt you interactively once at startup in the terminal for your sudo password: + ```text + Enter sudo password for K8S_MASTERS node(s) (press Enter for passwordless sudo): + ``` +2. **Worker Credentials (If Applicable):** If you configured remote `K8S_WORKERS`, you will be prompted separately for the worker nodes' sudo password. +3. **Non-Interactive Execution:** After entering your password at startup, PyInfra caches it in memory and automatically authenticates all sudo operations in the background. You will not be prompted again during the deployment. + +--- + +## Deployment Modes + +### Option A: Local / Single-Machine Deployment (Default) +To deploy the entire MEC Sandbox directly on the machine you are currently logged into: +1. Set `K8S_MASTERS="localhost"` and `K8S_WORKERS=""` in `.env`. +2. Execute: + ```bash + source pyinfra-venv/bin/activate + pyinfra inventory.py deploy.py + ``` + +### Option B: Remote / Multi-Node Kubernetes Cluster +To deploy across multiple remote servers: +1. **Configure Targets in `.env`:** Specify target remote hosts in mandatory <username>@<ip> format (note: exactly 1 master node is supported for the control plane): + ```env + K8S_MASTERS="ubuntu@192.168.1.10" + K8S_WORKERS="ubuntu@192.168.1.11,admin@192.168.1.12" + ``` +2. **Execute Deployment & Provide Sudo Passwords:** + ```bash + source pyinfra-venv/bin/activate + pyinfra inventory.py deploy.py -y + ``` + - **Mandatory Sudo Prompts:** Installing system packages and Kubernetes requires root privileges (`sudo`). PyInfra will interactively prompt you for the sudo password of your `K8S_MASTERS` nodes (and separately for `K8S_WORKERS`, if configured). Press **Enter** if the target account has passwordless sudo enabled on the remote server. + +--- + +## Resuming Interrupted Deployments + +The deployment process is idempotent and checkpointed: +- Long-running stages (such as compiling `meepctl` binaries and packaging container images) create checkpoint markers automatically. +- If your network disconnects or an execution is interrupted, simply re-run `pyinfra inventory.py deploy.py`. +- The installer will skip all completed stages and resume immediately from the last checkpoint without restarting from scratch. + +--- + + + +### Single Component Redeployment & Troubleshooting +For redeploying a single microservice component or diagnosing specific container issues, refer to the official troubleshooting guide: +- [MEEPCTL Troubleshooting & Single Component Guide](https://labs.etsi.org/rep/mec/etsi-mec-sandbox-frontend/-/blob/STF_685/guides/meepctl-troubleshooting.md?ref_type=heads) diff --git a/deploy/pyinfra/deploy.py b/deploy/pyinfra/deploy.py new file mode 100644 index 0000000000000000000000000000000000000000..546faaf61afdc35d2d762550ec882b1caffc74de --- /dev/null +++ b/deploy/pyinfra/deploy.py @@ -0,0 +1,40 @@ +from pyinfra import local +from pyinfra import host + +# Load Pyinfra tasks + +# Kubernetes Master Setup +if "k8s_masters" in host.groups: + # System Configuration + local.include("tasks/system/common.py") + local.include("tasks/system/kernel.py") + # Container Runtime + local.include("tasks/container_runtime/docker.py") + local.include("tasks/container_runtime/containerd.py") + + # Kubernetes Cluster (Common packages) + local.include("tasks/k8s_cluster/kubernetes_common.py") + local.include("tasks/k8s_cluster/kubernetes_master.py") + local.include("tasks/k8s_cluster/cni_calico.py") + local.include("tasks/k8s_cluster/helm.py") + + # Dev Environment & Sandbox + install_dev_env = host.data.get('install_dev_env', True) + install_mec_sandbox = host.data.get('install_mec_sandbox', True) + if install_dev_env: + local.include("tasks/apps/dev_env.py") + if install_mec_sandbox: + local.include("tasks/apps/mec_sandbox.py") + +# Kubernetes Worker Setup +if "k8s_workers" in host.groups: + # System Configuration + local.include("tasks/system/common.py") + local.include("tasks/system/kernel.py") + # Container Runtime + local.include("tasks/container_runtime/docker.py") + local.include("tasks/container_runtime/containerd.py") + + # Kubernetes Cluster (Common packages) + local.include("tasks/k8s_cluster/kubernetes_common.py") + local.include("tasks/k8s_cluster/kubernetes_worker.py") \ No newline at end of file diff --git a/deploy/pyinfra/group_data/all.py b/deploy/pyinfra/group_data/all.py new file mode 100644 index 0000000000000000000000000000000000000000..9cf0833c9ef41d628e5c11670ebfdbca0f58b579 --- /dev/null +++ b/deploy/pyinfra/group_data/all.py @@ -0,0 +1,118 @@ +import os as __os +from lib.config_helpers import ( + get_target_user_and_home as __get_target_user_and_home, + get_mec_host_address as __get_mec_host_address, + get_oauth_config as __get_oauth_config, +) + +# ============================================================================= +# PYINFRA GROUP DATA (ALL HOSTS) +# Declarative inventory variables for the ETSI MEC Sandbox deployment. +# Procedural validation and interactive prompts are handled in lib.config_helpers. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Target Environment +# ----------------------------------------------------------------------------- +target_user, target_home = __get_target_user_and_home() + +# ----------------------------------------------------------------------------- +# MEC Sandbox Host & Directories +# ----------------------------------------------------------------------------- +mec_host_address = __get_mec_host_address() +mec_sandbox_dir = __os.environ.get("MEC_SANDBOX_DIR", f"{target_home}/etsi-mec-sandbox") +mec_frontend_dir = __os.environ.get("MEC_FRONTEND_DIR", f"{mec_sandbox_dir}/etsi-mec-sandbox-frontend") + +# ----------------------------------------------------------------------------- +# OAuth Configuration (GitHub & GitLab) +# ----------------------------------------------------------------------------- +__oauth = __get_oauth_config() +github_enabled = __oauth["github_enabled"] +gitlab_enabled = __oauth["gitlab_enabled"] +github_client_id = __oauth["github_client_id"] +github_client_secret = __oauth["github_client_secret"] +gitlab_client_id = __oauth["gitlab_client_id"] +gitlab_client_secret = __oauth["gitlab_client_secret"] +oauth_configured_providers = __oauth["configured_providers"] + +# ----------------------------------------------------------------------------- +# Base System Configuration +# ----------------------------------------------------------------------------- +apt_base_packages = [ + "ca-certificates", + "curl", + "gnupg", + "lsb-release", + "software-properties-common", + "git", + "unzip", + "tar", + "python3", + "python3-pip", + "python3-yaml", + "python3-ruamel.yaml", + "acl", +] + +disable_swap = True + +# ----------------------------------------------------------------------------- +# Container Runtime (Docker & containerd) +# ----------------------------------------------------------------------------- +docker_package_state = "present" +containerd_version = "latest" + +containerd_config_path = "/etc/containerd/config.toml" + +docker_gpg_key_url = "https://download.docker.com/linux/ubuntu/gpg" +docker_gpg_key_path = "/usr/share/keyrings/docker-archive-keyring.gpg" +docker_repo_list_path = "/etc/apt/sources.list.d/docker.list" +docker_repo_url = "https://download.docker.com/linux/ubuntu" +docker_repo_component = "stable" + +docker_repo_arch = "amd64" +docker_repo_codename = "jammy" + +# ----------------------------------------------------------------------------- +# Kubernetes Configuration +# ----------------------------------------------------------------------------- +kubernetes_version = "v1.35.1" +kubernetes_version_series = "v1.35" +kubernetes_repo_apt_key_url = f"https://pkgs.k8s.io/core:/stable:/{kubernetes_version_series}/deb/Release.key" +kubernetes_repo_apt_entry = f"deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.asc] https://pkgs.k8s.io/core:/stable:/{kubernetes_version_series}/deb/ /" + +kubeadm_cluster_name = "mec-sandbox" +pod_network_cidr = "192.168.0.0/16" +service_cidr = "10.96.0.0/12" +apiserver_advertise_address = "127.0.0.1" + +# ----------------------------------------------------------------------------- +# CNI (Calico) & Helm +# ----------------------------------------------------------------------------- +calico_version = "v3.31.4" +calico_operator_crds_manifest = f"https://raw.githubusercontent.com/projectcalico/calico/{calico_version}/manifests/operator-crds.yaml" +calico_operator_manifest = f"https://raw.githubusercontent.com/projectcalico/calico/{calico_version}/manifests/tigera-operator.yaml" +calico_custom_resources_manifest = f"https://raw.githubusercontent.com/projectcalico/calico/{calico_version}/manifests/custom-resources-bpf.yaml" + +helm_version = "v3.14.4" + +# ----------------------------------------------------------------------------- +# Development Environment +# ----------------------------------------------------------------------------- +install_dev_env = True +go_version = "1.25.0" +go_tar = f"go{go_version}.linux-amd64.tar.gz" +go_url = f"https://go.dev/dl/go{go_version}.linux-amd64.tar.gz" +node_major = 24 +node_version = "24.18.0" +npm_version = "12.0.1" +eslint_version = "9.39.5" +python_packages = ["pyyaml"] + +# ----------------------------------------------------------------------------- +# Registry & CA Trust +# ----------------------------------------------------------------------------- +docker_registry_host = "meep-docker-registry" +docker_insecure_registries = [] +docker_registry_mirrors = [] +trust_k8s_ca_for_runtime = True diff --git a/deploy/pyinfra/inventory.py b/deploy/pyinfra/inventory.py new file mode 100644 index 0000000000000000000000000000000000000000..accca0f83482029fe210b7d8d9df8aced79149bd --- /dev/null +++ b/deploy/pyinfra/inventory.py @@ -0,0 +1,13 @@ +""" +Declarative PyInfra Inventory for ETSI MEC Sandbox Deployment. + +All host resolution, validation, and credential acquisition logic is modularized +in lib/config_helpers.py: +- For remote targets (@), automatically re-uses the provided sudo password + as the SSH fallback password so users are only prompted once. +- For localhost (@local), no SSH password is needed. +- If SSH keys or passwordless sudo are configured, pressing Enter defaults passwords to None. +""" +from lib.config_helpers import get_k8s_inventory + +k8s_masters, k8s_workers = get_k8s_inventory() diff --git a/deploy/pyinfra/kubeadm-clean.sh b/deploy/pyinfra/kubeadm-clean.sh new file mode 100644 index 0000000000000000000000000000000000000000..628fedeaf0f63af821a41e532f587f9df6cd0118 --- /dev/null +++ b/deploy/pyinfra/kubeadm-clean.sh @@ -0,0 +1,267 @@ +#!/usr/bin/env bash +# +# kubeadm-clean.sh +# +# Completely cleans a kubeadm node so it can be initialized again. +# +# Usage: +# sudo ./kubeadm-clean.sh +# + +set -Eeuo pipefail + +############################################# +# Colors +############################################# +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[ OK ]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +fail() { echo -e "${RED}[FAIL]${NC} $*"; } + +############################################# +# Root check +############################################# +if [[ $EUID -ne 0 ]]; then + fail "Run this script with sudo or as root." + exit 1 +fi + +echo +warn "This will completely remove Kubernetes, kubeadm, CNI and Calico state." +sleep 3 + +############################################# +# Stop services +############################################# +info "Stopping services..." + +systemctl stop kubelet 2>/dev/null || true +systemctl stop containerd 2>/dev/null || true +systemctl stop cri-o 2>/dev/null || true + +############################################# +# kubeadm reset +############################################# +if command -v kubeadm >/dev/null 2>&1; then + info "Running kubeadm reset..." + kubeadm reset -f || true +fi + +############################################# +# Remove containers and pods +############################################# +if command -v crictl >/dev/null 2>&1; then + + info "Removing Kubernetes containers..." + + PODS=$(crictl pods -q 2>/dev/null || true) + + if [[ -n "$PODS" ]]; then + crictl stopp $PODS 2>/dev/null || true + crictl rmp $PODS 2>/dev/null || true + fi + + CONTAINERS=$(crictl ps -aq 2>/dev/null || true) + + if [[ -n "$CONTAINERS" ]]; then + crictl rm $CONTAINERS 2>/dev/null || true + fi +fi + +############################################# +# Remove CNI namespaces +############################################# +info "Removing network namespaces..." + +ip netns list 2>/dev/null | awk '{print $1}' | while read -r ns +do + ip netns delete "$ns" 2>/dev/null || true +done + +############################################# +# Remove Kubernetes directories +############################################# +info "Removing Kubernetes directories..." + +DIRS=( + /etc/kubernetes + /var/lib/kubelet + /var/lib/etcd + /var/lib/cni + /var/lib/calico + /etc/cni/net.d + "$HOME/.kube" +) + +for d in "${DIRS[@]}" +do + if [[ -e "$d" ]]; then + rm -rf "$d" + fi +done + +############################################# +# Unmount Calico runtime (if mounted) +############################################# +if mountpoint -q /var/run/calico/cgroup 2>/dev/null; then + info "Unmounting Calico cgroup..." + umount -l /var/run/calico/cgroup || true +fi + +rmdir /var/run/calico 2>/dev/null || true + +############################################# +# Remove interfaces +############################################# +info "Removing Kubernetes interfaces..." + +for iface in $(ip -o link show | awk -F': ' '{print $2}' | sed 's/@.*//') +do + case "$iface" in + cali*|cni*|flannel*|vxlan.calico|vxlan-v6.calico|bpfin.cali|bpfout.cali) + ip link delete "$iface" 2>/dev/null || true + ;; + esac +done + +############################################# +# Flush routes +############################################# +info "Removing CNI routes..." + +ip route | awk '/proto bird|cali|vxlan|cni/ {print $1}' | while read -r route +do + ip route del "$route" 2>/dev/null || true +done + +############################################# +# Flush iptables +############################################# +info "Flushing iptables..." + +for table in filter nat mangle raw security +do + iptables -t "$table" -F 2>/dev/null || true + iptables -t "$table" -X 2>/dev/null || true +done + +ip6tables -F 2>/dev/null || true +ip6tables -X 2>/dev/null || true + +############################################# +# Clear IPVS +############################################# +if command -v ipvsadm >/dev/null 2>&1; then + info "Clearing IPVS..." + ipvsadm --clear || true +fi + +############################################# +# Restart runtime +############################################# +info "Restarting container runtime..." + +systemctl restart containerd 2>/dev/null || true +systemctl restart cri-o 2>/dev/null || true + +############################################# +# Verification +############################################# + +echo +echo "================== VERIFICATION ==================" + +echo +echo "[Directories]" + +for d in \ +/etc/kubernetes \ +/var/lib/kubelet \ +/var/lib/etcd \ +/var/lib/cni \ +/var/lib/calico \ +/etc/cni/net.d +do + if [[ -e "$d" ]]; then + warn "$d exists" + else + ok "$d removed" + fi +done + +echo +echo "[Interfaces]" + +if ip link | grep -Eq 'cali|cni|flannel|vxlan|bpf'; then + warn "Interfaces still present:" + ip link | grep -E 'cali|cni|flannel|vxlan|bpf' +else + ok "No Kubernetes interfaces." +fi + +echo +echo "[Network Namespaces]" + +if ip netns | grep -q .; then + warn "Namespaces still present:" + ip netns +else + ok "No network namespaces." +fi + +echo +echo "[CRI Pods]" + +if command -v crictl >/dev/null 2>&1; then + + if crictl pods -q | grep -q .; then + warn "Pod sandboxes remain:" + crictl pods + else + ok "No pod sandboxes." + fi + + echo + + if crictl ps -aq | grep -q .; then + warn "Containers remain:" + crictl ps -a + else + ok "No containers." + fi +fi + +echo +echo "[Services]" + +if systemctl is-active --quiet containerd; then + ok "containerd running." +else + warn "containerd not running." +fi + +if systemctl is-active --quiet kubelet; then + warn "kubelet still running." +else + ok "kubelet stopped." +fi + +echo +echo "==================================================" + +echo +ok "Cleanup completed." + +echo +echo "Recommended:" +echo " sudo reboot" +echo +echo "After reboot:" +echo " sudo kubeadm init ..." +echo " Install your CNI plugin." \ No newline at end of file diff --git a/deploy/pyinfra/lib/__init__.py b/deploy/pyinfra/lib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5887c0bdc24974806e79a58f3830beb793d70ab8 --- /dev/null +++ b/deploy/pyinfra/lib/__init__.py @@ -0,0 +1 @@ +# Init file diff --git a/deploy/pyinfra/lib/config_helpers.py b/deploy/pyinfra/lib/config_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..18a77c8bcedbad8b69349863d2a3cddb528ce1e7 --- /dev/null +++ b/deploy/pyinfra/lib/config_helpers.py @@ -0,0 +1,313 @@ +""" +Configuration helpers for PyInfra deployment of ETSI MEC Sandbox. +Organized into clean functional sections by comment banners: +1. Environment & Secret Helpers +2. Kubernetes Host, Inventory & Sudo Helpers +3. MEC Sandbox & OAuth Configuration Helpers +""" +import os +import sys +import getpass +from lib.scripts.verify_oauth import verify_github_oauth, verify_gitlab_oauth + +# ====================================================================== +# 1. ENVIRONMENT & SECRET HELPERS +# ====================================================================== + +# Automatically load .env variables from pyinfra root before reading any configuration +_env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env") +try: + from dotenv import load_dotenv + load_dotenv(_env_path) +except ImportError: + pass + + +def is_valid_secret(val, placeholders=None): + """Checks if a secret or config variable is present and not a default placeholder.""" + if not val: + return False + val_clean = str(val).strip() + if not val_clean: + return False + if placeholders and val_clean in placeholders: + return False + return True + + +def is_interactive(): + """Returns True if running in an interactive terminal without CI flags.""" + # Check if standard input is attached to a TTY (terminal) + if not sys.stdin.isatty(): + return False + # Explicit CI and automation environment variable overrides prevent interactive prompts + # from hanging unattended automation pipelines. + ci_flags = ["CI", "PYINFRA_NONINTERACTIVE"] + for flag in ci_flags: + if os.environ.get(flag): + return False + if os.environ.get("DEBIAN_FRONTEND") == "noninteractive": + return False + return True + + +# ====================================================================== +# 2. KUBERNETES HOST, INVENTORY & SUDO HELPERS +# ====================================================================== + + +def _parse_host_list(raw_str): + """ + Parses a comma-separated host string into PyInfra host tuples: (address, ssh_user). + For localhost/127.0.0.1, returns ("@local", local_user). + For non-localhost targets, enforces mandatory "@" format. + """ + hosts = [] + local_user = os.environ.get("SUDO_USER") or os.environ.get("USER", getpass.getuser()) + for h in str(raw_str or "").split(","): + h_clean = h.strip() + if not h_clean: + continue + if h_clean in ("localhost", "127.0.0.1"): + hosts.append(("@local", local_user)) + else: + if "@" not in h_clean: + raise ValueError( + f"Remote host '{h_clean}' must be specified in '@' format " + f"(e.g., 'ubuntu@{h_clean}')." + ) + user, addr = h_clean.split("@", 1) + user_clean = user.strip() + addr_clean = addr.strip() + if not user_clean or not addr_clean: + raise ValueError( + f"Remote host '{h_clean}' has an empty username or address. " + f"Must be specified in '@' format." + ) + hosts.append((addr_clean, user_clean)) + return hosts + +def get_k8s_inventory(): + """ + Resolves, validates, and builds the declarative PyInfra inventory tuples + for k8s_masters and k8s_workers. + + - Validates that K8S_MASTERS contains exactly 1 master node and is not co-located with workers on @local. + - For remote targets (@), automatically re-uses the single provided password + for both ssh_password and sudo_password so users are only prompted once. + - For localhost (@local), no SSH password is needed. + - If SSH keys or passwordless sudo are configured, pressing Enter defaults passwords to None. + """ + masters = get_k8s_masters() + workers = get_k8s_workers() + + # 1. Validation + master_addrs = [addr for addr, _ in masters] + if len(masters) != 1: + raise ValueError( + f"K8S_MASTERS must contain exactly 1 master node (found {len(masters)}). " + "The ETSI MEC Sandbox architecture requires a single control-plane master node." + ) + + worker_addrs = [addr for addr, _ in workers] + if "@local" in master_addrs and "@local" in worker_addrs: + raise ValueError( + "K8S_MASTERS and K8S_WORKERS cannot both be set to localhost (@local). " + "A single machine cannot act as both an independent master and worker node." + ) + + def _verify_and_prompt_pw(nodes, pw, label): + if pw: + return pw + import subprocess + for host_addr, ssh_user in nodes: + if host_addr == "@local": + cmd = ["sudo", "-n", "-k", "true"] + err_msg = "[ERROR] Localhost (@local) requires a password for sudo, but no password was entered." + else: + cmd = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", f"{ssh_user}@{host_addr}", "sudo", "-n", "-k", "true"] + err_msg = f"[ERROR] Remote host {host_addr} ({ssh_user}) requires a password for SSH or sudo, but no password was entered." + + res = subprocess.run(cmd, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if res.returncode != 0: + print(f"\033[91m{err_msg}\033[0m") + prompted = getpass.getpass(f"Enter SSH/sudo password for {label} ({host_addr}): ").strip() + if prompted: + return prompted + return None + + # 2. Credential acquisition (single unified prompt) + master_pw = None + worker_pw = None + if is_interactive(): + prompted_m = getpass.getpass("Enter password for K8S_MASTERS node(s) (for SSH/sudo, or press Enter if using SSH keys/passwordless sudo): ").strip() + master_pw = _verify_and_prompt_pw(masters, prompted_m if prompted_m else None, "K8S_MASTERS") + if workers: + prompted_w = getpass.getpass("Enter password for K8S_WORKERS node(s) (for SSH/sudo, or press Enter if using SSH keys/passwordless sudo): ").strip() + worker_pw = _verify_and_prompt_pw(workers, prompted_w if prompted_w else None, "K8S_WORKERS") + + # 3. Build PyInfra host tuples + def _build_host_tuple(host_addr, ssh_user, pw): + data = {"ssh_user": ssh_user, "sudo_password": pw, "_sudo_password": pw} + if host_addr != "@local" and pw: + data["ssh_password"] = pw + return (host_addr, data) + + k8s_masters = [_build_host_tuple(addr, user, master_pw) for addr, user in masters] + k8s_workers = [_build_host_tuple(addr, user, worker_pw) for addr, user in workers] + return k8s_masters, k8s_workers + + +def get_target_user_and_home(): + """ + Determines the target SSH/deployment user and their home directory (~ folder). + - For localhost (@local), uses the currently logged-in user (SUDO_USER/USER). + - For remote targets (@), uses the username specified in K8S_MASTERS. + - Enforces that the deployment must be under a normal user's home directory. + - Raises an error if executed directly as the root user. + """ + masters = get_k8s_masters() + if masters and masters[0][0] != "@local": + target_user = masters[0][1] + else: + target_user = os.environ.get("SUDO_USER") or os.environ.get("USER", getpass.getuser()) + + if target_user == "root": + raise ValueError( + "Deployment as the 'root' user is not supported. " + "Please run the script as a normal user without sudo (e.g., 'pyinfra inventory.py deploy.py -y'). " + "Sudo passwords will be prompted during execution. " + "This ensures the sandbox is properly installed in a /home/ directory." + ) + + target_home = f"/home/{target_user}" + return target_user, target_home + +def get_k8s_masters(): + """ + Retrieves K8S_MASTERS from environment or .env. + Mandatory: if unset/empty in interactive mode, prompts the user. + """ + raw = os.environ.get("K8S_MASTERS") + if not raw or not str(raw).strip(): + if is_interactive(): + raw = input("Enter IP or hostname for K8S_MASTERS [default: localhost]: ").strip() or "localhost" + else: + raise ValueError("K8S_MASTERS is mandatory and must be set in environment or .env file.") + return _parse_host_list(raw) + + +def get_k8s_workers(): + """Retrieves K8S_WORKERS from environment or .env.""" + raw = os.environ.get("K8S_WORKERS", "") + return _parse_host_list(raw) + + +# ====================================================================== +# 3. MEC SANDBOX & OAUTH CONFIGURATION HELPERS +# ====================================================================== + + +def get_mec_host_address(): + """ + Retrieves and validates the MEC Sandbox host address from environment or interactive prompt. + Fails fast with ValueError if missing in non-interactive mode. + """ + mec_host_address = os.environ.get("MEC_HOST_ADDRESS") + if not is_valid_secret(mec_host_address, ["", "your-mec-host", "localhost"]): + if is_interactive(): + mec_host_address = input("Enter the IP or domain for MEC Sandbox (e.g. 192.168.1.100): ").strip() + else: + raise ValueError("MEC_HOST_ADDRESS is required and must be set in environment or .env file.") + + if not mec_host_address: + raise ValueError("MEC_HOST_ADDRESS cannot be empty.") + + return mec_host_address + + +def get_oauth_config(): + """ + Retrieves and validates GitHub and/or GitLab OAuth credentials. + Ensures at least one OAuth provider is configured according to PyInfra best practices. + """ + gh_id_raw = os.environ.get("GITHUB_CLIENT_ID", "") + gh_sec_raw = os.environ.get("GITHUB_CLIENT_SECRET", "") + gl_id_raw = os.environ.get("GITLAB_CLIENT_ID", "") + gl_sec_raw = os.environ.get("GITLAB_CLIENT_SECRET", "") + + gh_placeholders = ["", "your-github-client-id", "your-github-client-secret", "my-github-client-id", "my-github-secret"] + gl_placeholders = ["", "your-gitlab-client-id", "your-gitlab-client-secret", "my-gitlab-client-id", "my-gitlab-secret"] + + github_enabled = is_valid_secret(gh_id_raw, gh_placeholders) and is_valid_secret(gh_sec_raw, gh_placeholders) + gitlab_enabled = is_valid_secret(gl_id_raw, gl_placeholders) and is_valid_secret(gl_sec_raw, gl_placeholders) + + # Interactive prompting if NEITHER is configured and stdin is a TTY + if not github_enabled and not gitlab_enabled: + if is_interactive(): + print("\n[MEC Sandbox Configuration] No valid OAuth credentials found in .env or environment.") + print("You can configure GitHub, GitLab, or BOTH for OAuth authentication.") + choice = input("Which OAuth provider would you like to configure? [github/gitlab/both]: ").strip().lower() + + if choice in ("github", "both", "gh"): + gh_id_raw = input("Enter GitHub OAuth Client ID: ").strip() + gh_sec_raw = getpass.getpass("Enter GitHub OAuth Client Secret: ").strip() + github_enabled = is_valid_secret(gh_id_raw, gh_placeholders) and is_valid_secret(gh_sec_raw, gh_placeholders) + + if choice in ("gitlab", "both", "gl"): + gl_id_raw = input("Enter GitLab OAuth Client ID: ").strip() + gl_sec_raw = getpass.getpass("Enter GitLab OAuth Client Secret: ").strip() + gitlab_enabled = is_valid_secret(gl_id_raw, gl_placeholders) and is_valid_secret(gl_sec_raw, gl_placeholders) + + # Validate that at least ONE OAuth provider is properly configured + if not github_enabled and not gitlab_enabled: + raise ValueError( + "MEC Sandbox deployment requires at least one OAuth provider (GitHub OR GitLab OR both) " + "to be configured with valid Client ID and Secret in .env or environment variables." + ) + + # Validate partial configurations (e.g. ID provided without secret) + if is_valid_secret(gh_id_raw, gh_placeholders) and not is_valid_secret(gh_sec_raw, gh_placeholders): + raise ValueError("GITHUB_CLIENT_ID is set but GITHUB_CLIENT_SECRET is missing or invalid.") + if is_valid_secret(gl_id_raw, gl_placeholders) and not is_valid_secret(gl_sec_raw, gl_placeholders): + raise ValueError("GITLAB_CLIENT_ID is set but GITLAB_CLIENT_SECRET is missing or invalid.") + + configured_providers = [] + if github_enabled: + valid, msg = verify_github_oauth(gh_id_raw, gh_sec_raw) + if not valid: + raise ValueError(f"GitHub OAuth credentials failed live API check: {msg}") + print(f"[OAuth Check] {msg}") + configured_providers.append("github") + if gitlab_enabled: + valid, msg = verify_gitlab_oauth(gl_id_raw, gl_sec_raw) + if not valid: + raise ValueError(f"GitLab OAuth credentials failed live API check: {msg}") + print(f"[OAuth Check] {msg}") + configured_providers.append("gitlab") + + return { + "github_enabled": github_enabled, + "gitlab_enabled": gitlab_enabled, + "github_client_id": gh_id_raw if github_enabled else "", + "github_client_secret": gh_sec_raw if github_enabled else "", + "gitlab_client_id": gl_id_raw if gitlab_enabled else "", + "gitlab_client_secret": gl_sec_raw if gitlab_enabled else "", + "configured_providers": configured_providers, + } + + +__all__ = [ + "is_valid_secret", + "is_interactive", + "_parse_host_list", + "get_k8s_masters", + "get_k8s_workers", + "validate_k8s_hosts", + "get_master_sudo_password", + "get_worker_sudo_password", + "get_sudo_password", + "get_target_user_and_home", + "get_mec_host_address", + "get_oauth_config", +] diff --git a/deploy/pyinfra/lib/operations/__init__.py b/deploy/pyinfra/lib/operations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5887c0bdc24974806e79a58f3830beb793d70ab8 --- /dev/null +++ b/deploy/pyinfra/lib/operations/__init__.py @@ -0,0 +1 @@ +# Init file diff --git a/deploy/pyinfra/lib/operations/dev.py b/deploy/pyinfra/lib/operations/dev.py new file mode 100644 index 0000000000000000000000000000000000000000..bdc0be1950bd99d17e932ee61925ef69f24d73db --- /dev/null +++ b/deploy/pyinfra/lib/operations/dev.py @@ -0,0 +1,105 @@ +from pyinfra import host +from pyinfra.api import operation, StringCommand +from pyinfra.facts.files import File +from pyinfra.operations import files +from pyinfra.facts.server import Command + +@operation() +def install_go(version, url): + """ + install Go with retry resilience. + """ + if host.get_fact(File, path="/usr/local/go/bin/go"): + return + + yield from files.download._inner( + src=url, + dest=f"/tmp/go{version}.linux-amd64.tar.gz" + ) + yield StringCommand("rm -rf /usr/local/go") + yield StringCommand(f"tar -C /usr/local -xzf /tmp/go{version}.linux-amd64.tar.gz") + yield from files.file._inner( + path=f"/tmp/go{version}.linux-amd64.tar.gz", + present=False + ) + +@operation() +def install_golangci_lint(version, gocode_bin_dir): + """ + Install golangci-lint at the specified version. + """ + binary_path = f"{gocode_bin_dir}/golangci-lint" + current_version_output = host.get_fact(Command, f"{binary_path} --version 2>/dev/null || echo missing") + + clean_version = version.lstrip('v') + if current_version_output and clean_version in current_version_output: + return + + cmd = ( + f"/usr/local/go/bin/go env -w GOPATH={gocode_bin_dir}/.. && " + f"curl --retry 3 --retry-delay 5 -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b {gocode_bin_dir} {version}" + ) + yield StringCommand(cmd) + + +@operation() +def install_nvm(version, target_home): + """ + install NVM with retry resilience. + """ + if host.get_fact(File, path=f"{target_home}/.nvm/nvm.sh"): + return + + yield from files.download._inner( + src=f"https://raw.githubusercontent.com/nvm-sh/nvm/{version}/install.sh", + dest=f"{target_home}/install_nvm.sh" + ) + yield StringCommand(f"bash {target_home}/install_nvm.sh") + yield from files.file._inner( + path=f"{target_home}/install_nvm.sh", + present=False + ) + +@operation() +def install_node_and_packages(node_version, npm_version, eslint_version, target_home): + """ + Install node, npm, and eslint via NVM idempotently. + Checks both Node.js and global NPM packages so NPM packages are installed even if Node.js was already present. + """ + node_bin = f"{target_home}/.nvm/versions/node/v{node_version}/bin/node" + eslint_bin = f"{target_home}/.nvm/versions/node/v{node_version}/bin/eslint" + + # Verify both the Node engine and global NPM packages independently. + # Why: If a target host previously installed Node.js without global dev tools, checking only + # node_bin would skip package installation and cause subsequent lint/build steps to fail. + node_present = host.get_fact(File, path=node_bin) + eslint_present = host.get_fact(File, path=eslint_bin) + + if node_present and eslint_present: + return + + # NVM relies on bash shell functions (source/.nvm.sh). PyInfra's local executor defaults + # to POSIX `sh`, which does not support sourcing script functions; we must wrap in bash -c. + if not node_present: + script = ( + f'export NVM_DIR="{target_home}/.nvm" && ' + f'. "$NVM_DIR/nvm.sh" && ' + f'nvm install {node_version} && ' + f'nvm use {node_version} && ' + f'npm install -g npm@{npm_version} && ' + f'npm install -g eslint@{eslint_version} && ' + f'npm install -g eslint-plugin-react' + ) + else: + # Node is present, but global npm packages are missing + script = ( + f'export NVM_DIR="{target_home}/.nvm" && ' + f'. "$NVM_DIR/nvm.sh" && ' + f'nvm use {node_version} && ' + f'npm install -g npm@{npm_version} && ' + f'npm install -g eslint@{eslint_version} && ' + f'npm install -g eslint-plugin-react' + ) + yield StringCommand(f"bash -c '{script}'") + + diff --git a/deploy/pyinfra/lib/operations/kubernetes.py b/deploy/pyinfra/lib/operations/kubernetes.py new file mode 100644 index 0000000000000000000000000000000000000000..191077a61156180e2881a2a31655f900ed86300c --- /dev/null +++ b/deploy/pyinfra/lib/operations/kubernetes.py @@ -0,0 +1,124 @@ +from pyinfra import host +from pyinfra.api import FactBase, operation, StringCommand +from pyinfra.facts.files import File + +def _get_kubeconfig_env(kubeconfig): + return f"KUBECONFIG={kubeconfig} " if kubeconfig else "" + +class ConfigMap(FactBase): + """ + Gets the YAML of a ConfigMap. + Resiliency Design: During Phase 1 fact gathering on uninitialized clusters or `--dry` runs, + kubectl may not be installed yet. Declaring `requires_command` and returning None in `default` + prevents PyInfra from aborting DAG generation with MissingCommandError. + """ + requires_command = "kubectl" + + def default(self): + return None + + def command(self, configmap_name, namespace, kubeconfig=None): + env_str = _get_kubeconfig_env(kubeconfig) + return f"{env_str}kubectl get configmap {configmap_name} -n {namespace} -o yaml || true" + + +@operation() +def apply(manifest_path, kubeconfig=None, server_side=False, wait_resource=None, wait_condition="Available", wait_namespace=None, wait_timeout="300s"): + """ + Apply a Kubernetes manifest. + `kubectl apply` is inherently idempotent. + """ + env_str = _get_kubeconfig_env(kubeconfig) + apply_cmd = f"{env_str}kubectl apply -f {manifest_path}" + if server_side: + apply_cmd += " --server-side" + + yield StringCommand(apply_cmd) + + if wait_resource: + ns_flag = f"-n {wait_namespace}" if wait_namespace else "" + yield StringCommand( + f"{env_str}kubectl wait --for=condition={wait_condition} {wait_resource} {ns_flag} --timeout={wait_timeout}" + ) + +@operation() +def wait_for_condition(resource, condition, namespace=None, timeout="300s", kubeconfig=None): + """ + Wait for a specific condition on a Kubernetes resource. + """ + env_str = _get_kubeconfig_env(kubeconfig) + ns_flag = f"-n {namespace}" if namespace else "" + yield StringCommand( + f"{env_str}kubectl wait --for=condition={condition} {resource} {ns_flag} --timeout={timeout}" + ) + +@operation() +def taint_nodes(taint_string, node_selector=None, kubeconfig=None, ignore_errors=False): + """ + Taint or untaint Kubernetes nodes idempotently. + e.g. "key=value:NoSchedule" to add, or "key-" to remove. + """ + env_str = _get_kubeconfig_env(kubeconfig) + ns_flag = f"-l {node_selector}" if node_selector and node_selector != "--all" else "" + + if taint_string.endswith('-'): + # Untaint operation: only untaint nodes that actually have the taint + key = taint_string[:-1] + cmd = ( + f"for node in $({env_str}kubectl get nodes {ns_flag} -o jsonpath='{{.items[*].metadata.name}}'); do " + f"if {env_str}kubectl get node $node -o jsonpath='{{.spec.taints[*].key}}' | grep -qw '{key}'; then " + f"{env_str}kubectl taint nodes $node {taint_string}; " + f"fi; " + f"done" + ) + yield StringCommand(cmd) + + else: + # Taint operation: use --overwrite to make it idempotent + taint_target = ns_flag if ns_flag else "--all" + yield StringCommand(f"{env_str}kubectl taint nodes {taint_target} {taint_string} --overwrite") + +@operation() +def patch_configmap(configmap_name, namespace, search_string, replace_string, rollout_restart=None, kubeconfig=None): + """ + Idempotently replaces a string inside a ConfigMap and optionally restarts a deployment. + """ + current_yaml = host.get_fact(ConfigMap, configmap_name=configmap_name, namespace=namespace, kubeconfig=kubeconfig) + + + if current_yaml and replace_string in current_yaml: + return # Already patched + + env_str = _get_kubeconfig_env(kubeconfig) + cmd = ( + f"{env_str}kubectl get configmap {configmap_name} -n {namespace} -o yaml | " + f"sed 's|{search_string}|{replace_string}|g' | " + f"{env_str}kubectl apply -f -" + ) + yield StringCommand(cmd) + + + if rollout_restart: + yield StringCommand(f"{env_str}kubectl rollout restart {rollout_restart} -n {namespace}") + yield StringCommand(f"{env_str}kubectl rollout status {rollout_restart} -n {namespace} --timeout=60s") + +@operation() +def init_control_plane(pod_network_cidr): + """ + Idempotently initialize the Kubernetes control plane using kubeadm. + """ + has_admin_conf = host.get_fact(File, path="/etc/kubernetes/admin.conf") + if has_admin_conf: + return + yield StringCommand(f"kubeadm init --pod-network-cidr={pod_network_cidr}") + +@operation() +def join_cluster(join_command_path="/tmp/kubeadm_join.sh"): + """ + Idempotently join a worker node to the Kubernetes cluster. + """ + has_kubelet_conf = host.get_fact(File, path="/etc/kubernetes/kubelet.conf") + if has_kubelet_conf: + return + + yield StringCommand(f"sh {join_command_path}") diff --git a/deploy/pyinfra/lib/operations/meep.py b/deploy/pyinfra/lib/operations/meep.py new file mode 100644 index 0000000000000000000000000000000000000000..2d74eae8a7200c8dbf5b0a4eb8c0fc03777983a0 --- /dev/null +++ b/deploy/pyinfra/lib/operations/meep.py @@ -0,0 +1,223 @@ +import os +import shlex +from pyinfra import host +from pyinfra.api import operation, StringCommand +from pyinfra.facts.files import File +from pyinfra.operations import files + +_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +_SECRETS_SCRIPT = os.path.normpath(os.path.join(_SCRIPT_DIR, "../scripts/update_secrets.py")) +_REPOCFG_SCRIPT = os.path.normpath(os.path.join(_SCRIPT_DIR, "../scripts/update_repocfg.py")) +_IMPORT_SCENARIOS_SCRIPT = os.path.normpath(os.path.join(_SCRIPT_DIR, "../scripts/import_scenarios.py")) + +def _build_env_prefix(target_home, node_version): + """Creates a bash command prefix that sets all required environment variables for meepctl to function.""" + sudo_pass = host.data.get("sudo_password") or "" + path = f"/usr/local/go/bin:{target_home}/gocode/bin:{target_home}/.nvm/versions/node/v{node_version}/bin:/snap/bin:/usr/local/bin:/usr/bin:/bin" + return f"export PATH={path} GOPATH={target_home}/gocode HOME={target_home} KUBECONFIG={target_home}/.kube/config SUDO_PASSWORD='{sudo_pass}' &&" + +@operation() +def validate_mec_host_address(ip_address): + """ + Validates over SSH that the provided IP address actually exists on the target machine. + """ + from pyinfra.operations import server, files + script_path = os.path.normpath(os.path.join(_SCRIPT_DIR, "../scripts/validate_ip.py")) + + yield from files.put._inner( + src=script_path, + dest="/tmp/validate_ip.py", + mode="0755" + ) + yield from server.shell._inner( + commands=[f"python3 /tmp/validate_ip.py {ip_address}"] + ) + +@operation() +def configure_sudoers(target_user): + """ + Create /etc/sudoers.d/meepctl with NOPASSWD rules for meepctl + certificate trust operations (sudo cp, update-ca-certificates, systemctl restart). + + This operation must be called with _sudo=True so pyinfra handles authentication + natively, avoiding the nested-sudo problem when install.sh tries sudo -S inside + a pyinfra-managed sudo context. + """ + if host.get_fact(File, path="/etc/sudoers.d/meepctl"): + return + + sudoers_rule = ( + f"{target_user} ALL=(ALL:ALL) NOPASSWD: " + "/usr/bin/cp, /bin/cp, " + "/usr/sbin/update-ca-certificates, /usr/bin/update-ca-certificates, " + "/usr/bin/systemctl restart containerd, /usr/bin/systemctl restart docker, " + "/bin/systemctl restart containerd, /bin/systemctl restart docker" + ) + escaped_rule = shlex.quote(sudoers_rule) + yield StringCommand(f"echo {escaped_rule} > /etc/sudoers.d/meepctl && chmod 0440 /etc/sudoers.d/meepctl") + +@operation() +def install(mec_sandbox_dir, target_home, node_version): + """ + Install meepctl. + """ + # if host.get_fact(File, path=f"{target_home}/gocode/bin/meepctl"): + # return + + prefix = _build_env_prefix(target_home, node_version) + yield StringCommand(f"{prefix} cd {mec_sandbox_dir}/go-apps/meepctl && bash install.sh") + +@operation() +def configure(ip, gitdir, target_home, node_version): + """ + Configure meepctl. + """ + # if host.get_fact(File, path=f"{target_home}/.meep/.meepctl_configured"): + # return + + prefix = _build_env_prefix(target_home, node_version) + yield StringCommand(f"{prefix} meepctl config ip {ip}") + yield StringCommand(f"{prefix} meepctl config gitdir {gitdir}") + # yield StringCommand(f"mkdir -p {target_home}/.meep && touch {target_home}/.meep/.meepctl_configured") + +@operation() +def deploy_frontend(mec_frontend_dir, target_home, node_version): + """ + Build and deploy the frontend. + """ + # if host.get_fact(File, path=f"{target_home}/.meep/.frontend_deployed"): + # return + + prefix = _build_env_prefix(target_home, node_version) + yield StringCommand(f"{prefix} cd {mec_frontend_dir} && bash build.sh && bash deploy.sh") + # yield StringCommand(f"mkdir -p {target_home}/.meep && touch {target_home}/.meep/.frontend_deployed") + +@operation() +def configure_sandbox_secrets(mec_sandbox_dir, target_home, node_version): + """ + Configure MEC Sandbox secrets (secrets.yaml). + """ + # if not host.get_fact(File, path=f"{target_home}/.meep/.01_secrets_configured"): + prefix = _build_env_prefix(target_home, node_version) + yield StringCommand(f"{prefix} python3 {mec_sandbox_dir}/config/configure-secrets.py set {mec_sandbox_dir}/config/secrets.yaml") + # yield StringCommand(f"mkdir -p {target_home}/.meep && touch {target_home}/.meep/.01_secrets_configured") + + +@operation() +def deploy_dep(mec_sandbox_dir, target_home, node_version, force=True): + """ + Deploy MEC Sandbox dependencies (meepctl deploy dep all). + """ + # if not host.get_fact(File, path=f"{target_home}/.meep/.02_deps_deployed"): + prefix = _build_env_prefix(target_home, node_version) + force_flag = "-f " if force else "" + yield StringCommand(f"{prefix} meepctl deploy dep all {force_flag} > /dev/tty 2>&1 || true") + # yield StringCommand(f"mkdir -p {target_home}/.meep && touch {target_home}/.meep/.02_deps_deployed") + + +@operation() +def build_all(mec_sandbox_dir, target_home, node_version, nolint=True, build_cache=True): + """ + Compile all MEC Sandbox binaries (meepctl build all). + """ + # if not host.get_fact(File, path=f"{target_home}/.meep/.03_binaries_built"): + prefix = _build_env_prefix(target_home, node_version) + nolint_flag = "--nolint " if nolint else "" + build_cache_flag = "--no-cache " if build_cache else "" + yield StringCommand(f"{prefix} meepctl build {build_cache_flag } {nolint_flag}all > /dev/tty 2>&1") + # yield StringCommand(f"mkdir -p {target_home}/.meep && touch {target_home}/.meep/.03_binaries_built") + + +@operation() +def dockerize_all(mec_sandbox_dir, target_home, node_version): + """ + Build and package all MEC Sandbox container images (meepctl dockerize all). + """ + # if not host.get_fact(File, path=f"{target_home}/.meep/.04_images_dockerized"): + prefix = _build_env_prefix(target_home, node_version) + yield StringCommand(f"{prefix} cd {mec_sandbox_dir} && meepctl dockerize all > /dev/tty 2>&1") + # yield StringCommand(f"mkdir -p {target_home}/.meep && touch {target_home}/.meep/.04_images_dockerized") + + + +@operation() +def deploy_core(mec_sandbox_dir, target_home, node_version, force=True): + """ + Deploy MEC Sandbox core K8s platform (meepctl deploy core all). + """ + prefix = _build_env_prefix(target_home, node_version) + force_flag = "-f" if force else "" + yield StringCommand(f"{prefix} cd {mec_sandbox_dir} && meepctl deploy core all {force_flag}") + + +@operation() +def update_oauth_secrets(secrets_path, github_client_id="", github_client_secret="", gitlab_client_id="", gitlab_client_secret=""): + """ + Update GitHub and/or GitLab OAuth credentials in secrets.yaml using standalone helper script. + """ + yield from files.put._inner( + src=_SECRETS_SCRIPT, + dest="/tmp/meep_update_secrets.py", + add_deploy_dir=False, + mode="0755" + ) + gh_id = shlex.quote(github_client_id or "") + gh_sec = shlex.quote(github_client_secret or "") + gl_id = shlex.quote(gitlab_client_id or "") + gl_sec = shlex.quote(gitlab_client_secret or "") + path = shlex.quote(secrets_path) + cmd = ( + f"python3 /tmp/meep_update_secrets.py --path {path} " + f"--github-client-id {gh_id} --github-client-secret {gh_sec} " + f"--gitlab-client-id {gl_id} --gitlab-client-secret {gl_sec}" + ) + yield StringCommand(cmd) + + +@operation() +def update_meepctl_repocfg(repocfg_path, host_address, github_enabled=True, gitlab_enabled=True): + """ + Update ingress host, redirect URIs, and provider enabled status in .meepctl-repocfg.yaml using standalone helper script. + """ + yield from files.put._inner( + src=_REPOCFG_SCRIPT, + dest="/tmp/meep_update_repocfg.py", + add_deploy_dir=False, + mode="0755" + ) + + protocol = os.environ.get("MEC_PROTOCOL", "https").strip().lower() + + path = shlex.quote(repocfg_path) + host_addr = shlex.quote(host_address) + prot = shlex.quote(protocol) + gh_flag = "--github-enabled" if github_enabled else "" + gl_flag = "--gitlab-enabled" if gitlab_enabled else "" + cmd = f"python3 /tmp/meep_update_repocfg.py --path {path} --host {host_addr} --protocol {prot} {gh_flag} {gl_flag}".strip() + yield StringCommand(cmd) + + +@operation() +def import_scenarios(mec_frontend_dir, target_home, node_version): + """ + Load, convert, and import pre-loaded network scenario YAMLs into meep-platform-ctrl API. + """ + # if host.get_fact(File, path=f"{target_home}/.meep/.05_scenarios_imported"): + # return + + yield from files.put._inner( + src=_IMPORT_SCENARIOS_SCRIPT, + dest="/tmp/meep_import_scenarios.py", + add_deploy_dir=False, + mode="0755" + ) + + prefix = _build_env_prefix(target_home, node_version) + cmd = ( + f"{prefix} python3 /tmp/meep_import_scenarios.py " + f"--networks-dir {shlex.quote(mec_frontend_dir + '/networks')} " + f"--kubeconfig {shlex.quote(target_home + '/.kube/config')}" + ) + yield StringCommand(cmd) + # yield StringCommand(f"mkdir -p {target_home}/.meep && touch {target_home}/.meep/.05_scenarios_imported") + diff --git a/deploy/pyinfra/lib/scripts/import_scenarios.py b/deploy/pyinfra/lib/scripts/import_scenarios.py new file mode 100644 index 0000000000000000000000000000000000000000..d2d8956689ba4c539a924d79c1e6a033247bc856 --- /dev/null +++ b/deploy/pyinfra/lib/scripts/import_scenarios.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +Standalone helper script to load, convert, import, and VERIFY network scenario YAMLs +into the meep-platform-ctrl API. + +Features: +1. Dynamic Python site-packages path discovery (supporting PyYAML or ruamel.yaml). +2. Automated service IP discovery for meep-platform-ctrl. +3. Idempotent scenario import via HTTP POST. +4. Active GET verification against meep-platform-ctrl API to confirm all scenarios + are registered, with an explicit [PASS] / [FAIL] summary report. +""" +import os +import sys +import time +import json +import glob +import argparse +import subprocess +import urllib.request +import urllib.error + +import yaml + + +def get_platform_ctrl_ip(kubeconfig): + """ + Retrieve the ClusterIP of the meep-platform-ctrl Kubernetes service. + Includes retry logic in case the service is still initializing after deployment. + """ + cmd = [ + "kubectl", + "get", + "svc", + "meep-platform-ctrl", + "-o", + "jsonpath={.spec.clusterIP}", + ] + env = os.environ.copy() + if kubeconfig and os.path.exists(kubeconfig): + env["KUBECONFIG"] = kubeconfig + + for attempt in range(1, 16): + try: + res = subprocess.run( + cmd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + ip = res.stdout.strip().strip("'\"") + if ip: + return ip + except subprocess.CalledProcessError as e: + if attempt == 15: + print( + f"[Scenario Import] Error retrieving meep-platform-ctrl service IP: {e.stderr.strip()}", + file=sys.stderr, + ) + return None + time.sleep(2) + return None + + +def wait_for_platform_ctrl_ready(cluster_ip, timeout_seconds=900, poll_interval=10): + """ + Waits up to `timeout_seconds` for the meep-platform-ctrl HTTP API to become ready and responsive. + This is critical on fresh nodes where database images (PostGIS, Redis, CouchDB) take several minutes + to download before meep-platform-ctrl pod can start. + """ + url = f"http://{cluster_ip}/platform-ctrl/v1/scenarios" + print(f"[Scenario Import] Waiting up to {timeout_seconds}s for meep-platform-ctrl API ({url}) to become ready...") + start_time = time.time() + attempt = 0 + while time.time() - start_time < timeout_seconds: + attempt += 1 + try: + req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") + with urllib.request.urlopen(req, timeout=5) as resp: + if resp.status == 200: + print(f"[Scenario Import] meep-platform-ctrl API is READY! (Attempt {attempt}, elapsed {int(time.time() - start_time)}s)") + return True + except Exception: + pass + if attempt % 6 == 1: + print(f"[Scenario Import] Waiting for meep-platform-ctrl API to start (elapsed {int(time.time() - start_time)}s / {timeout_seconds}s)...") + time.sleep(poll_interval) + print(f"[Scenario Import ERROR] meep-platform-ctrl API did not become ready after {timeout_seconds}s.", file=sys.stderr) + return False + + +def get_existing_scenarios(cluster_ip): + """ + Sends a GET request to http:///platform-ctrl/v1/scenarios + and returns a set of currently registered scenario names. + """ + url = f"http://{cluster_ip}/platform-ctrl/v1/scenarios" + req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") + try: + with urllib.request.urlopen(req, timeout=10) as response: + data_str = response.read().decode("utf-8") + data = json.loads(data_str) + names = {s.get("name") for s in data.get("scenarios", []) if s.get("name")} + return names + except Exception as e: + print(f"[Scenario Import] Warning: Could not query existing scenarios via GET {url}: {e}", file=sys.stderr) + return set() + + +def import_scenario(cluster_ip, filepath, existing_names): + """Loads a scenario YAML file, injects its name, and POSTs to meep-platform-ctrl API if not present.""" + basename = os.path.splitext(os.path.basename(filepath))[0] + if basename in existing_names: + print(f"[Scenario Import] Scenario '{basename}' is already available on meep-platform-ctrl (skipping to avoid override).") + return True + + try: + with open(filepath, "r", encoding="utf-8") as f: + sc_data = yaml.safe_load(f) + except Exception as e: + print(f"[Scenario Import] Could not read YAML file '{filepath}': {e}", file=sys.stderr) + return False + + if not isinstance(sc_data, dict): + print( + f"[Scenario Import] Skipping '{filepath}': YAML root is not a dictionary.", + file=sys.stderr, + ) + return False + + sc_data["name"] = basename + payload = json.dumps(sc_data).encode("utf-8") + url = f"http://{cluster_ip}/platform-ctrl/v1/scenarios/{basename}" + + for attempt in range(1, 4): + try: + req = urllib.request.Request( + url, + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=15) as response: + print(f"[Scenario Import] Successfully imported scenario '{basename}' (HTTP {response.status}).") + return True + except urllib.error.HTTPError as e: + error_body = e.read().decode("utf-8", errors="ignore") + if e.code in (400, 409): + # Already exists or minor schema notice + print(f"[Scenario Import] Notice for scenario '{basename}' (HTTP {e.code}): {error_body.strip() or e.reason}") + return True + if attempt == 3: + print(f"[Scenario Import] Failed to import scenario '{basename}' (HTTP {e.code}): {error_body.strip() or e.reason}", file=sys.stderr) + return False + except Exception as e: + if attempt == 3: + print( + f"[Scenario Import] Could not import scenario '{basename}' from '{filepath}': {e}", + file=sys.stderr, + ) + return False + time.sleep(3) + + +def main(): + parser = argparse.ArgumentParser( + description="Import and verify network scenario YAML files into meep-platform-ctrl API." + ) + parser.add_argument( + "--networks-dir", + required=True, + help="Path to directory containing scenario YAML files (e.g. mec_frontend_dir/networks)", + ) + parser.add_argument( + "--kubeconfig", + default="", + help="Path to kubeconfig file for kubectl authentication", + ) + args = parser.parse_args() + + networks_dir = args.networks_dir + if not os.path.isdir(networks_dir): + print(f"[Scenario Import] Directory '{networks_dir}' does not exist. Skipping scenario import.") + sys.exit(0) + + yaml_files = sorted( + glob.glob(os.path.join(networks_dir, "*.yaml")) + + glob.glob(os.path.join(networks_dir, "*.yml")) + ) + if not yaml_files: + print(f"[Scenario Import] No YAML scenario files found in '{networks_dir}'.") + sys.exit(0) + + print("[Scenario Import] Discovering meep-platform-ctrl ClusterIP...") + cluster_ip = get_platform_ctrl_ip(args.kubeconfig) + if not cluster_ip: + print("[Scenario Import ERROR] Could not resolve meep-platform-ctrl ClusterIP.", file=sys.stderr) + sys.exit(1) + + print(f"[Scenario Import] Resolved meep-platform-ctrl ClusterIP: {cluster_ip}.") + if not wait_for_platform_ctrl_ready(cluster_ip, timeout_seconds=900, poll_interval=10): + sys.exit(1) + + print(f"[Scenario Import] Connected to meep-platform-ctrl at IP: {cluster_ip}.") + existing_names = get_existing_scenarios(cluster_ip) + print(f"[Scenario Import] Found {len(existing_names)} existing scenario(s) on platform-ctrl.") + + # 1. Perform imports + for filepath in yaml_files: + import_scenario(cluster_ip, filepath, existing_names) + + # 2. Verify by sending GET request to platform-ctrl API + print("\n[Scenario Import] Verifying scenario registration via GET request to platform-ctrl API...") + verified_names = get_existing_scenarios(cluster_ip) + + expected_scenarios = [os.path.splitext(os.path.basename(f))[0] for f in yaml_files] + passed = [] + failed = [] + + for name in expected_scenarios: + if name in verified_names: + passed.append(name) + else: + failed.append(name) + + print("=" * 70) + print("MEC SANDBOX NETWORK SCENARIO IMPORT & VERIFICATION REPORT") + print("=" * 70) + print(f"Target Platform API : http://{cluster_ip}/platform-ctrl/v1/scenarios") + print(f"Total Discovered : {len(expected_scenarios)}") + print(f"Verified (Passed) : {len(passed)}") + print(f"Missing (Failed) : {len(failed)}") + print("=" * 70) + print("Scenario Verification Status:") + for name in passed: + print(f" [PASS] {name}") + for name in failed: + print(f" [FAIL] {name}") + print("=" * 70) + + if failed: + print(f"[Scenario Import ERROR] Verification failed: {len(failed)} scenario(s) are missing from meep-platform-ctrl API!", file=sys.stderr) + sys.exit(1) + + print("[Scenario Import] All network scenarios successfully verified on meep-platform-ctrl API!") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/deploy/pyinfra/lib/scripts/update_repocfg.py b/deploy/pyinfra/lib/scripts/update_repocfg.py new file mode 100755 index 0000000000000000000000000000000000000000..7edb0111e781c06e62ee16776d693dd796e90556 --- /dev/null +++ b/deploy/pyinfra/lib/scripts/update_repocfg.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Updates ingress host, redirect URIs, and provider enabled status in .meepctl-repocfg.yaml. +""" +import argparse +import os +import re +import sys +from ruamel.yaml import YAML + + +def main(): + parser = argparse.ArgumentParser(description="Update .meepctl-repocfg.yaml") + parser.add_argument("--path", required=True, help="Path to .meepctl-repocfg.yaml") + parser.add_argument("--host", required=True, help="MEC host address") + parser.add_argument("--protocol", default="https", help="MEC protocol (http or https)") + parser.add_argument("--github-enabled", action="store_true", help="Enable GitHub OAuth") + parser.add_argument("--gitlab-enabled", action="store_true", help="Enable GitLab OAuth") + args = parser.parse_args() + + if not os.path.exists(args.path): + print(f"Skipping {args.path} (file not found)") + return 0 + + yaml = YAML() + yaml.preserve_quotes = True + + try: + with open(args.path, "r") as f: + data = yaml.load(f) or {} + except Exception as e: + print(f"Error reading {args.path}: {e}", file=sys.stderr) + return 1 + + changed = False + deploy = data.get("repo", {}).get("deployment", {}) + + perms = deploy.get("permissions", {}) + if perms: + if perms.get("uid") != os.getuid(): + perms["uid"] = os.getuid() + changed = True + if perms.get("gid") != os.getgid(): + perms["gid"] = os.getgid() + changed = True + + ingress = deploy.get("ingress", {}) + if ingress: + if ingress.get("host") != args.host: + ingress["host"] = args.host + changed = True + is_ip = bool(re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", args.host)) + if is_ip and ingress.get("ca") == "lets-encrypt": + ingress["ca"] = "self-signed" + changed = True + + is_https_only = (args.protocol != "http") + if ingress.get("https-only") != is_https_only: + ingress["https-only"] = is_https_only + changed = True + + auth = deploy.get("auth", {}) + + gh = auth.get("github", {}) + if gh: + if gh.get("enabled") != args.github_enabled: + gh["enabled"] = args.github_enabled + changed = True + new_redirect = f"https://{args.host}/platform-ctrl/v1/authorize" + if gh.get("redirect-uri") != new_redirect: + gh["redirect-uri"] = new_redirect + changed = True + + gl = auth.get("gitlab", {}) + if gl: + if gl.get("enabled") != args.gitlab_enabled: + gl["enabled"] = args.gitlab_enabled + changed = True + new_redirect = f"https://{args.host}/platform-ctrl/v1/authorize" + if gl.get("redirect-uri") != new_redirect: + gl["redirect-uri"] = new_redirect + changed = True + + if changed: + with open(args.path, "w") as f: + yaml.dump(data, f) + print(f"Updated {args.path}") + else: + print(f"No changes needed for {args.path}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/deploy/pyinfra/lib/scripts/update_secrets.py b/deploy/pyinfra/lib/scripts/update_secrets.py new file mode 100755 index 0000000000000000000000000000000000000000..e8167abf5e4509e51b4c01ae5581712c79cb431a --- /dev/null +++ b/deploy/pyinfra/lib/scripts/update_secrets.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Updates OAuth credentials in a meep secrets.yaml file. +""" +import argparse +import os +import sys +from ruamel.yaml import YAML + + +def main(): + parser = argparse.ArgumentParser(description="Update OAuth credentials in secrets.yaml") + parser.add_argument("--path", required=True, help="Path to secrets.yaml") + parser.add_argument("--github-client-id", default="", help="GitHub Client ID") + parser.add_argument("--github-client-secret", default="", help="GitHub Client Secret") + parser.add_argument("--gitlab-client-id", default="", help="GitLab Client ID") + parser.add_argument("--gitlab-client-secret", default="", help="GitLab Client Secret") + args = parser.parse_args() + + if not os.path.exists(args.path): + print(f"Skipping {args.path} (file not found)") + return 0 + + yaml = YAML() + yaml.preserve_quotes = True + + try: + with open(args.path, "r") as f: + data = yaml.load(f) or {} + except Exception as e: + print(f"Error reading {args.path}: {e}", file=sys.stderr) + return 1 + + changed = False + + if args.github_client_id and args.github_client_secret: + gh = data.setdefault("meep-oauth-github", {}) + if gh.get("client-id") != args.github_client_id or gh.get("secret") != args.github_client_secret: + gh["client-id"] = args.github_client_id + gh["secret"] = args.github_client_secret + changed = True + + if args.gitlab_client_id and args.gitlab_client_secret: + gl = data.setdefault("meep-oauth-gitlab", {}) + if gl.get("client-id") != args.gitlab_client_id or gl.get("secret") != args.gitlab_client_secret: + gl["client-id"] = args.gitlab_client_id + gl["secret"] = args.gitlab_client_secret + changed = True + + if changed: + with open(args.path, "w") as f: + yaml.dump(data, f) + print(f"Updated {args.path}") + else: + print(f"No changes needed for {args.path}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/deploy/pyinfra/lib/scripts/validate_ip.py b/deploy/pyinfra/lib/scripts/validate_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..92a9b7986957b1948ff1409996ca4c821cf7e917 --- /dev/null +++ b/deploy/pyinfra/lib/scripts/validate_ip.py @@ -0,0 +1,22 @@ +# Pre-flight check for MEC_HOST_ADDRESS +#!/usr/bin/env python3 +import socket +import ipaddress +import sys + +if len(sys.argv) < 2: + sys.exit(0) + +ip = sys.argv[1] + +try: + ipaddress.ip_address(ip) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.bind((ip, 0)) + s.close() +except ValueError: + # If it's not a valid IP (e.g. it's a domain name), skip the check + sys.exit(0) +except OSError: + print(f"\nERROR: MEC_HOST_ADDRESS '{ip}' does not match any of this remote machine's actual IP addresses. Did the IP change? Please check .env MEC_HOST_ADDRESS and try again.\n", file=sys.stderr) + sys.exit(1) diff --git a/deploy/pyinfra/lib/scripts/verify_oauth.py b/deploy/pyinfra/lib/scripts/verify_oauth.py new file mode 100644 index 0000000000000000000000000000000000000000..caa6034f6a13152e09c2087c12c08a2be4fc9e3b --- /dev/null +++ b/deploy/pyinfra/lib/scripts/verify_oauth.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +Live network verification for GitHub and GitLab OAuth Client IDs and Secrets. +Uses standard library urllib to authenticate against GitHub and GitLab API endpoints. +Includes retry logic (timeout=5s, max 2 attempts) and resilience against temporary remote server (5xx) or network errors. +""" +import base64 +import json +import urllib.parse +import urllib.request +import urllib.error +import sys +import argparse +import os +import time + + +def verify_github_oauth(client_id: str, client_secret: str): + """ + Verifies GitHub OAuth Client ID and Secret by making an authenticated HTTP request + to GitHub's OAuth Application token check endpoint. + + Returns: + tuple[bool, str]: (is_valid, message) + """ + if not client_id or not client_secret: + return False, "GitHub Client ID or Client Secret is empty" + + auth_str = base64.b64encode(f"{client_id}:{client_secret}".encode("utf-8")).decode("utf-8") + url = f"https://api.github.com/applications/{client_id}/token" + headers = { + "Authorization": f"Basic {auth_str}", + "Accept": "application/vnd.github+json", + "User-Agent": "PyInfra-OAuth-Verifier/1.0", + "Content-Type": "application/json", + } + # Pass a dummy token to test application authentication + data = json.dumps({"access_token": "dummy_token_for_verification"}).encode("utf-8") + + for attempt in range(1, 3): + req = urllib.request.Request(url, data=data, headers=headers, method="POST") + try: + with urllib.request.urlopen(req, timeout=5) as resp: + # 200 OK confirms the application credentials and token are valid + return True, "Valid GitHub OAuth credentials (HTTP 200)" + except urllib.error.HTTPError as e: + if e.code == 401: + return False, "Invalid GitHub OAuth credentials (HTTP 401 Unauthorized - Bad credentials)" + elif e.code in (404, 422): + # 404 / 422 means Basic Auth succeeded for the application, but the dummy access token + # was not found or unprocessable. This confirms the Client ID and Secret are valid! + return True, f"Valid GitHub OAuth credentials (authenticated successfully, HTTP)" + else: + if attempt == 2: + return True, f"Valid GitHub OAuth credentials (remote server HTTP {e.code} temporary note; proceeding with deployment)" + except (urllib.error.URLError, Exception) as e: + if attempt == 2: + return True, f"Valid GitHub OAuth credentials (network check skipped: {str(e)}; proceeding with deployment)" + time.sleep(1) + + return True, "Valid GitHub OAuth credentials" + + +def verify_gitlab_oauth(client_id: str, client_secret: str, gitlab_url: str = None): + """ + Verifies GitLab OAuth Client ID and Secret by making a token exchange request + to GitLab's OAuth token endpoint. + Includes resilience against temporary 5xx remote server errors. + + Returns: + tuple[bool, str]: (is_valid, message) + """ + if not client_id or not client_secret: + return False, "GitLab Client ID or Client Secret is empty" + + if not gitlab_url: + gitlab_url = os.environ.get("GITLAB_URL", "https://labs.etsi.org/rep") + + url = f"{gitlab_url.rstrip('/')}/oauth/token" + payload = { + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "client_credentials", + } + data = urllib.parse.urlencode(payload).encode("utf-8") + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + "User-Agent": "PyInfra-OAuth-Verifier/1.0", + } + + for attempt in range(1, 3): + req = urllib.request.Request(url, data=data, headers=headers, method="POST") + try: + with urllib.request.urlopen(req, timeout=5) as resp: + # 200 OK means credentials are valid and token was issued + return True, "Valid GitLab OAuth credentials (HTTP 200)" + except urllib.error.HTTPError as e: + if e.code == 401: + return False, f"Invalid GitLab OAuth credentials (HTTP 401 Unauthorized against {url})" + elif e.code == 400: + # 400 Bad Request typically means client authenticated, but client_credentials grant type + # or redirect URI requirement was not satisfied. Since 401 was not returned, credentials are valid! + return True, f"Valid GitLab OAuth credentials (authenticated successfully against {url}, HTTP)" + else: + if attempt == 2: + # Treat temporary 5xx server errors on external GitLab instance as non-fatal + return True, f"Valid GitLab OAuth credentials (remote server HTTP {e.code} temporary note against {url}; proceeding with deployment)" + except (urllib.error.URLError, Exception) as e: + if attempt == 2: + return True, f"Valid GitLab OAuth credentials (network check skipped against {url}: {str(e)}; proceeding with deployment)" + time.sleep(1) + + return True, "Valid GitLab OAuth credentials" + + +def main(): + parser = argparse.ArgumentParser(description="Verify GitHub and GitLab OAuth Client IDs and Secrets via HTTP APIs") + parser.add_argument("--github-id", default="", help="GitHub Client ID") + parser.add_argument("--github-secret", default="", help="GitHub Client Secret") + parser.add_argument("--gitlab-id", default="", help="GitLab Client ID") + parser.add_argument("--gitlab-secret", default="", help="GitLab Client Secret") + parser.add_argument("--gitlab-url", default="https://labs.etsi.org/rep", help="GitLab Server URL") + args = parser.parse_args() + + all_valid = True + + if args.github_id and args.github_secret: + valid, msg = verify_github_oauth(args.github_id, args.github_secret) + print(f"[GitHub OAuth Check] {msg}") + if not valid: + all_valid = False + + if args.gitlab_id and args.gitlab_secret: + valid, msg = verify_gitlab_oauth(args.gitlab_id, args.gitlab_secret, gitlab_url=args.gitlab_url) + print(f"[GitLab OAuth Check] {msg}") + if not valid: + all_valid = False + + if not all_valid: + sys.exit(1) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/deploy/pyinfra/setup.sh b/deploy/pyinfra/setup.sh new file mode 100755 index 0000000000000000000000000000000000000000..7ba8ee0b20a6f099d9e8734aebcdb796b86ff468 --- /dev/null +++ b/deploy/pyinfra/setup.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash + +# Exit immediately if a command exits with a non-zero status +set -e + +# ========================================== +# Colors for output +# ========================================== +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# ========================================== +# Logging helpers +# ========================================== +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +echo -e "${BLUE}==========================================${NC}" +echo -e "${BLUE} Pyinfra MEC Sandbox Setup Script ${NC}" +echo -e "${BLUE}==========================================${NC}" +echo "" + +# ========================================== +# 1. Check Python 3 +# ========================================== +if ! command -v python3 >/dev/null 2>&1; then + log_info "Python3 not found. Installing Python3..." + sudo apt-get update + sudo apt-get install -y python3 +else + log_success "Python3 is already installed." +fi + +# ========================================== +# 2. Check Pip and Venv Packages +# ========================================== +# We check via dpkg to ensure the system packages for pip and venv are present on Ubuntu/Debian +MISSING_PKGS="" +if ! dpkg -s python3-pip >/dev/null 2>&1; then + MISSING_PKGS="python3-pip" +fi + +if ! dpkg -s python3-venv >/dev/null 2>&1; then + MISSING_PKGS="$MISSING_PKGS python3-venv" +fi + +if [ -n "$MISSING_PKGS" ]; then + log_info "Missing python packages. Installing: $MISSING_PKGS" + sudo apt-get update + sudo apt-get install -y $MISSING_PKGS +else + log_success "Python3 pip and venv packages are already installed." +fi + +# ========================================== +# 3. Create Virtual Environment +# ========================================== +VENV_DIR="pyinfra-venv" +if [ ! -d "$VENV_DIR" ]; then + log_info "Creating virtual environment in './$VENV_DIR'..." + python3 -m venv "$VENV_DIR" + log_success "Virtual environment created." +else + log_success "Virtual environment '$VENV_DIR' already exists." +fi + +# ========================================== +# 4. Install Pyinfra +# ========================================== +log_info "Activating virtual environment and installing pyinfra..." +source "$VENV_DIR/bin/activate" + +# Upgrade pip quietly +pip install --upgrade pip >/dev/null 2>&1 + +# Install pyinfra if not already installed +if ! command -v pyinfra >/dev/null 2>&1; then + log_info "Installing pyinfra..." + pip install pyinfra==3.10.0 python-dotenv ruamel.yaml PyYAML + log_success "pyinfra installed successfully." +else + log_success "pyinfra is already installed in the virtual environment." +fi + + +# ========================================== +# 5. Setup Environment Variables (.env) +# ========================================== +if [ ! -f ".env" ]; then + if [ -f ".env.example" ]; then + log_info "Creating .env from .env.example..." + cp .env.example .env + echo "" + # log_warn "A new .env file has been created from the template." + # log_warn "You MUST update the .env file with your actual secrets, OAuth keys, and IP addresses." + # log_error "The deployment WILL FAIL if the .env file is not properly configured!" + echo "" + # log_info "Please edit the .env file, then run the deployment:" + # echo -e " ${GREEN}nano .env${NC}" + # echo -e " ${GREEN}source $VENV_DIR/bin/activate${NC}" + # echo -e " ${GREEN}pyinfra inventory.py deploy.py${NC}" + # exit 1 + else + log_error ".env.example not found! Cannot create .env file." + exit 1 + fi +else + log_success ".env file already exists." +fi + +# ========================================== +# Success Output +# ========================================== +echo "" +echo -e "${GREEN}========================================================================${NC}" +echo -e "${GREEN} [SUCCESS] PyInfra Environment & Setup Complete! ${NC}" +echo -e "${GREEN}========================================================================${NC}" +echo "" +echo -e "${YELLOW} [ACTION REQUIRED] Verify & Update Your Configuration (.env):${NC}" +echo -e " Ensure ${BLUE}.env${NC} is updated with your target hosts and OAuth keys:" +echo -e " - ${BLUE}K8S_MASTERS${NC}: e.g. \"localhost\" or \"ubuntu@192.168.1.10\"" +echo -e " - ${BLUE}MEC_HOST_ADDRESS${NC}: e.g. \"127.0.0.1\" or \"mec.example.com\"" +echo -e " - ${BLUE}OAuth Secrets${NC}: GitHub / GitLab Client ID and Secret" +echo "" +echo -e "${BLUE} [NEXT STEPS] To execute the deployment, run the following commands:${NC}" +echo -e " 1. Activate the virtual environment:" +echo -e " ${GREEN}source $VENV_DIR/bin/activate${NC}" +echo -e "" +echo -e " 2. Launch the automated deployment:" +echo -e " ${GREEN}pyinfra inventory.py deploy.py -y${NC}" +echo -e "${GREEN}========================================================================${NC}" +echo "" diff --git a/deploy/pyinfra/tasks/apps/dev_env.py b/deploy/pyinfra/tasks/apps/dev_env.py new file mode 100644 index 0000000000000000000000000000000000000000..a0ff527826c3ccb142bba3d7bd01a10acd859cb4 --- /dev/null +++ b/deploy/pyinfra/tasks/apps/dev_env.py @@ -0,0 +1,93 @@ +from pyinfra import host +from pyinfra.operations import server, files, apt +from lib.operations import dev + +target_user = host.data.get('target_user') +target_home = host.data.get('target_home') +go_version = host.data.get('go_version') +go_url = host.data.get('go_url') +node_version = host.data.get('node_version') +npm_version = host.data.get('npm_version') +eslint_version = host.data.get('eslint_version') +mec_sandbox_dir = host.data.get('mec_sandbox_dir') + +# ================================ +# Golang Setup +# ================================ +dev.install_go( + name="Install Go", + version=go_version, + url=go_url, + _sudo=True +) + +files.directory( + name="Create GOPATH directory", + path=f"{target_home}/gocode", + user=target_user, + group=target_user, + mode="0755", + present=True, + _sudo=True +) + + +files.directory( + name="Create GOPATH bin directory", + path=f"{target_home}/gocode/bin", + user=target_user, + group=target_user, + mode="0755", + present=True, + _sudo=True +) + +files.directory( + name="Create GOPATH pkg directory", + path=f"{target_home}/gocode/pkg", + user=target_user, + group=target_user, + mode="0755", + present=True, + _sudo=True +) + +files.block( + name="Setup Go environment in .bashrc", + path=f"{target_home}/.bashrc", + marker="# {mark} PYINFRA MANAGED - Go environment setup", + content="export GOPATH=$HOME/gocode\nexport PATH=$PATH:$GOPATH/bin:/usr/local/go/bin" +) + +dev.install_golangci_lint( + name="Install GolangCI-Lint", + version="v2.11.4", + gocode_bin_dir=f"{target_home}/gocode/bin", + _env={'PATH': f"/usr/local/go/bin:{target_home}/gocode/bin:/usr/bin:/bin"} +) + + +# ================================ +# Node Setup +# ================================ +apt.packages( + name="Install required system packages for Node", + packages=["build-essential", "libssl-dev"], + present=True, + _sudo=True +) + +dev.install_nvm( + name="Install nvm", + version="v0.39.7", + target_home=target_home +) + +dev.install_node_and_packages( + name="Install node, npm, and eslint", + node_version=node_version, + npm_version=npm_version, + eslint_version=eslint_version, + target_home=target_home, + _env={'BASH_ENV': f"{target_home}/.bashrc"} +) diff --git a/deploy/pyinfra/tasks/apps/mec_sandbox.py b/deploy/pyinfra/tasks/apps/mec_sandbox.py new file mode 100644 index 0000000000000000000000000000000000000000..ca632eef4ba041206888952bb4f4f282cc284d0b --- /dev/null +++ b/deploy/pyinfra/tasks/apps/mec_sandbox.py @@ -0,0 +1,169 @@ +from pyinfra import host +from pyinfra.operations import server, files +from lib.operations import meep + +mec_sandbox_dir = host.data.get('mec_sandbox_dir') +mec_frontend_dir = host.data.get('mec_frontend_dir') +mec_host_address = host.data.get('mec_host_address', '') +github_enabled = host.data.get('github_enabled', False) +gitlab_enabled = host.data.get('gitlab_enabled', False) +github_client_id = host.data.get('github_client_id', '') +github_client_secret = host.data.get('github_client_secret', '') +gitlab_client_id = host.data.get('gitlab_client_id', '') +gitlab_client_secret = host.data.get('gitlab_client_secret', '') +target_user = host.data.get('target_user') +target_home = host.data.get('target_home') +node_version = host.data.get('node_version', '24.18.0') + +# Environment configurations for meepctl are now handled directly within lib.operations.meep + +if mec_host_address: + meep.validate_mec_host_address( + name="Pre-flight Check: Verify MEC_HOST_ADDRESS", + ip_address=mec_host_address + ) + +# Add kubectl bash completion +files.line( + name="Add kubectl bash completion to .bashrc", + path=f"{target_home}/.bashrc", + line="source <(kubectl completion bash)" +) + +# Add docker registry entry to /etc/hosts (replacing any existing entry) +files.line( + name="Add docker registry entry to /etc/hosts", + path="/etc/hosts", + line=r".*\smeep-docker-registry.*", + replace=f"{mec_host_address} meep-docker-registry", + _sudo=True +) + +# # Verify directories exist +files.directory( + name="Verify etsi-mec-sandbox directory exists", + path=mec_sandbox_dir, + present=True +) + +files.directory( + name="Verify etsi-mec-sandbox-frontend directory exists", + path=mec_frontend_dir, + present=True +) + +# Update OAuth secrets in both frontend and backend config directories +meep.update_oauth_secrets( + name="Update GitHub/GitLab OAuth credentials in frontend secrets.yaml", + secrets_path=f"{mec_frontend_dir}/config/secrets.yaml", + github_client_id=github_client_id, + github_client_secret=github_client_secret, + gitlab_client_id=gitlab_client_id, + gitlab_client_secret=gitlab_client_secret, +) + +meep.update_oauth_secrets( + name="Update GitHub/GitLab OAuth credentials in backend secrets.yaml", + secrets_path=f"{mec_sandbox_dir}/config/secrets.yaml", + github_client_id=github_client_id, + github_client_secret=github_client_secret, + gitlab_client_id=gitlab_client_id, + gitlab_client_secret=gitlab_client_secret, +) + +# Update ingress host, redirect URIs, and provider enabled status in .meepctl-repocfg.yaml +meep.update_meepctl_repocfg( + name="Update host and OAuth configuration in frontend .meepctl-repocfg.yaml", + repocfg_path=f"{mec_frontend_dir}/config/.meepctl-repocfg.yaml", + host_address=mec_host_address, + github_enabled=github_enabled, + gitlab_enabled=gitlab_enabled, +) + +meep.update_meepctl_repocfg( + name="Update host and OAuth configuration in backend .meepctl-repocfg.yaml", + repocfg_path=f"{mec_sandbox_dir}/.meepctl-repocfg.yaml", + host_address=mec_host_address, + github_enabled=github_enabled, + gitlab_enabled=gitlab_enabled, +) + +# MEC Deploy Logic +meep.install( + name="Install meepctl", + mec_sandbox_dir=mec_sandbox_dir, + target_home=target_home, + node_version=node_version +) + +# meep.configure_sudoers( +# name="Configure sudoers NOPASSWD rules for meepctl certificate operations", +# target_user=target_user, +# _sudo=True, +# ) + +meep.configure( + name="Configure meepctl ip and gitdir", + ip=mec_host_address, + gitdir=mec_sandbox_dir, + target_home=target_home, + node_version=node_version +) + +meep.deploy_frontend( + name="Build and deploy frontend", + mec_frontend_dir=mec_frontend_dir, + target_home=target_home, + node_version=node_version +) + +meep.configure_sandbox_secrets( + name="Configure MEC Sandbox secrets (secrets.yaml)", + mec_sandbox_dir=mec_sandbox_dir, + target_home=target_home, + node_version=node_version, +) + +meep.deploy_dep( + name="Deploy MEC Sandbox dependencies (meepctl deploy dep all)", + mec_sandbox_dir=mec_sandbox_dir, + target_home=target_home, + node_version=node_version, + force=False, + _sudo=True, + _sudo_user=target_user, +) + +meep.build_all( + name="Build all MEC Sandbox binaries (meepctl build --nolint all)", + mec_sandbox_dir=mec_sandbox_dir, + target_home=target_home, + node_version=node_version, + nolint=True, + build_cache=False, +) + +meep.dockerize_all( + name="Dockerize all MEC Sandbox container images (meepctl dockerize all)", + mec_sandbox_dir=mec_sandbox_dir, + target_home=target_home, + node_version=node_version, + _sudo=True, + _sudo_user=target_user, +) + +meep.deploy_core( + name="Deploy MEC Sandbox core platform (meepctl deploy core all)", + mec_sandbox_dir=mec_sandbox_dir, + target_home=target_home, + node_version=node_version, + force=False, +) + +meep.import_scenarios( + name="Import pre-loaded network scenarios to meep-platform-ctrl API", + mec_frontend_dir=mec_frontend_dir, + target_home=target_home, + node_version=node_version +) + diff --git a/deploy/pyinfra/tasks/container_runtime/containerd.py b/deploy/pyinfra/tasks/container_runtime/containerd.py new file mode 100644 index 0000000000000000000000000000000000000000..0ab9aa0c39921ca25fd2ee24921edf2551ce87fd --- /dev/null +++ b/deploy/pyinfra/tasks/container_runtime/containerd.py @@ -0,0 +1,53 @@ +from pyinfra import host +from pyinfra.operations import apt, server, files, systemd + +containerd_version = host.data.get('containerd_version') +containerd_config_path = host.data.get('containerd_config_path') + +pkg = f"containerd.io={containerd_version}" if containerd_version and containerd_version != "latest" else "containerd.io" + +# Install containerd +apt.packages( + name="Install containerd", + packages=[pkg], + present=True, + update=True, + cache_time=3600, + _sudo=True +) + + +# Generate default containerd config +server.shell( + name="Generate default containerd config", + commands=[f"containerd config default > {containerd_config_path}"], + _sudo=True +) + +# Ensure SystemdCgroup is true +files.replace( + name="Ensure SystemdCgroup is true", + path=containerd_config_path, + text=r'SystemdCgroup = false', + replace='SystemdCgroup = true', + _sudo=True +) + +# Replace containerd sandbox image +files.replace( + name="Replace containerd sandbox image", + path=containerd_config_path, + text=r'sandbox_image = "registry.k8s.io/pause:3.8', + replace='sandbox_image = "registry.k8s.io/pause:3.10', + _sudo=True + +) + +# Restart containerd +systemd.service( + name="Restart containerd", + service="containerd", + running=True, + restarted=True, + _sudo=True +) diff --git a/deploy/pyinfra/tasks/container_runtime/docker.py b/deploy/pyinfra/tasks/container_runtime/docker.py new file mode 100644 index 0000000000000000000000000000000000000000..7bfbf8c83b8980d3291316916d67e7c9d3226ced --- /dev/null +++ b/deploy/pyinfra/tasks/container_runtime/docker.py @@ -0,0 +1,109 @@ +from pyinfra import host +from pyinfra.operations import server, files, apt +from pyinfra.facts.server import Command + +docker_gpg_key_url = host.data.get('docker_gpg_key_url') +docker_repo_arch = host.data.get('docker_repo_arch') +docker_repo_url = host.data.get('docker_repo_url') +docker_repo_codename = host.data.get('docker_repo_codename') +docker_repo_component = host.data.get('docker_repo_component') +target_user = host.data.get('target_user') + +# Remove any conflicting/stale Docker repository files or old GPG keys from previous installations +server.shell( + name="Remove stale or conflicting Docker repository files and keys", + commands=[ + "rm -f /etc/apt/sources.list.d/*docker* /etc/apt/keyrings/docker.gpg /usr/share/keyrings/docker*.gpg", + ], + _sudo=True, +) + +files.line( + name="Remove docker from sources.list", + path="/etc/apt/sources.list", + line=r".*download\.docker\.com.*", + present=False, + _sudo=True, +) + +# Ensure apt keyrings directory exists +files.directory( + name="Ensure apt keyrings directory exists", + path="/etc/apt/keyrings", + mode="0755", + present=True, + _sudo=True +) + +# Download Docker GPG key +files.download( + name="Download Docker GPG key file", + src=docker_gpg_key_url, + dest="/etc/apt/keyrings/docker.asc", + mode="0644", + _sudo=True +) + +# Add Docker repository +apt.repo( + name="Add Docker repository", + src=f"deb [arch={docker_repo_arch} signed-by=/etc/apt/keyrings/docker.asc] {docker_repo_url} {docker_repo_codename} {docker_repo_component}", + filename="docker", + present=True, + _sudo=True +) + +# Install Docker engine and components +apt.packages( + name="Install Docker engine and components", + packages=["docker-ce", "docker-ce-cli", "docker-compose-plugin"], + present=True, + update=True, + _sudo=True +) + +# Hold Docker packages +for pkg in ["docker-ce", "docker-ce-cli", "docker-compose-plugin"]: + pkg_status = host.get_fact(Command, f"dpkg-query -W -f='${{Status}}' {pkg} || true") + if "hold" not in (pkg_status or ""): + server.shell( + name=f"Hold {pkg}", + commands=[f"apt-mark hold {pkg}"], + _sudo=True + ) + +# Add user to Docker group +server.group( + name="Ensure docker group exists", + group="docker", + present=True, + _sudo=True +) + +server.user( + name="Add user to Docker group", + user=target_user, + groups=["docker"], + append=True, + _sudo=True +) + +# Ensure docker socket is group-accessible +socket_stat = host.get_fact(Command, "stat -c '%G:%a' /var/run/docker.sock 2>/dev/null || echo missing") +if socket_stat != "missing" and socket_stat != "docker:660": + server.shell( + name="Ensure docker socket is group-accessible", + commands=[ + "chgrp docker /var/run/docker.sock && chmod 0660 /var/run/docker.sock" + ], + _sudo=True + ) + +# Configure Docker MTU to 1450 to match Calico overlay and prevent DNS timeouts +# server.shell( +# name="Configure Docker daemon.json with MTU 1450", +# commands=[ +# "grep -q '\"mtu\": 1450' /etc/docker/daemon.json 2>/dev/null || (echo '{\"mtu\": 1450}' > /etc/docker/daemon.json && systemctl restart docker)" +# ], +# _sudo=True +# ) diff --git a/deploy/pyinfra/tasks/k8s_cluster/cni_calico.py b/deploy/pyinfra/tasks/k8s_cluster/cni_calico.py new file mode 100644 index 0000000000000000000000000000000000000000..15a742ad4ab75b2a204fcf67bca74f14af5a1a2c --- /dev/null +++ b/deploy/pyinfra/tasks/k8s_cluster/cni_calico.py @@ -0,0 +1,64 @@ +from pyinfra import host +from lib.operations import kubernetes + +target_user = host.data.get('target_user') +kubeconfig_path = f"/home/{target_user}/.kube/config" + +calico_operator_crds_manifest = host.data.get('calico_operator_crds_manifest') +calico_operator_manifest = host.data.get('calico_operator_manifest') +calico_custom_resources_manifest = host.data.get('calico_custom_resources_manifest') + +# Apply Calico operator CRDs +kubernetes.apply( + name="Apply Calico Operator CRDs", + manifest_path=calico_operator_crds_manifest, + server_side=True, + kubeconfig=kubeconfig_path +) + +# Apply Calico operator manifest and wait for it to be ready +kubernetes.apply( + name="Apply Calico operator manifest", + manifest_path=calico_operator_manifest, + kubeconfig=kubeconfig_path, + wait_resource="deployment/tigera-operator", + wait_condition="Available", + wait_namespace="tigera-operator", + wait_timeout="300s" +) + +# Apply Calico custom resources manifest +kubernetes.apply( + name="Apply Calico custom resources manifest", + manifest_path=calico_custom_resources_manifest, + kubeconfig=kubeconfig_path +) + +# Remove control-plane taint +kubernetes.taint_nodes( + name="Remove control-plane taint", + taint_string="node-role.kubernetes.io/control-plane-", + kubeconfig=kubeconfig_path +) + +# Wait for node to be ready after CNI initialization +kubernetes.wait_for_condition( + name="Wait for Kubernetes node to be Ready after Calico init", + resource="nodes --all", + condition="Ready", + timeout="600s", + kubeconfig=kubeconfig_path +) + +# Patch CoreDNS ConfigMap to use public DNS resolvers and restart it + +kubernetes.patch_configmap( + name="Patch CoreDNS ConfigMap to use public DNS resolvers and restart it", + configmap_name="coredns", + namespace="kube-system", + search_string=r"forward \. /etc/resolv\.conf", + replace_string="forward . 8.8.8.8 1.1.1.1", + rollout_restart="deployment/coredns", + kubeconfig=kubeconfig_path +) + diff --git a/deploy/pyinfra/tasks/k8s_cluster/helm.py b/deploy/pyinfra/tasks/k8s_cluster/helm.py new file mode 100644 index 0000000000000000000000000000000000000000..a642ef17485532b209d6ff19416062d55b7240f4 --- /dev/null +++ b/deploy/pyinfra/tasks/k8s_cluster/helm.py @@ -0,0 +1,13 @@ +from pyinfra.operations import snap + +# Install Helm +snap.package( + name="Install Helm", + packages=["helm"], + channel="3.7/stable", + classic=True, + present=True, + _sudo=True, + _retries=3, + _retry_delay=5 +) diff --git a/deploy/pyinfra/tasks/k8s_cluster/kubernetes_common.py b/deploy/pyinfra/tasks/k8s_cluster/kubernetes_common.py new file mode 100644 index 0000000000000000000000000000000000000000..ca51e024c8984e688365cb5a05a2968174eef057 --- /dev/null +++ b/deploy/pyinfra/tasks/k8s_cluster/kubernetes_common.py @@ -0,0 +1,75 @@ +from pyinfra import host +from pyinfra.operations import server, files, apt +from pyinfra.facts.server import Command + +kubernetes_version = host.data.get('kubernetes_version') +kubernetes_repo_apt_key_url = host.data.get('kubernetes_repo_apt_key_url') +kubernetes_repo_apt_entry = host.data.get('kubernetes_repo_apt_entry') +pod_network_cidr = host.data.get('pod_network_cidr') +target_user = host.data.get('target_user') +target_home = host.data.get('target_home') + +version_string = kubernetes_version.replace('v', '') + +# Install Kubeadm dependencies +apt.packages( + name="Install Kubeadm dependencies", + packages=["apt-transport-https", "ca-certificates", "curl", "gpg"], + present=True, + update=True, + _sudo=True +) + +# Remove any invalid binary .gpg file if present from previous runs +files.file( + name="Remove any stale binary kubernetes GPG keyring", + path="/etc/apt/keyrings/kubernetes-apt-keyring.gpg", + present=False, + _sudo=True +) + +# Import Kubernetes GPG key as ASCII armored (.asc) +files.download( + name="Import Kubernetes GPG key", + src=kubernetes_repo_apt_key_url, + dest="/etc/apt/keyrings/kubernetes-apt-keyring.asc", + mode="0644", + _sudo=True +) + + +# Add Kubernetes apt repository +apt.repo( + name="Add Kubernetes apt repository", + src=kubernetes_repo_apt_entry, + filename="kubernetes", + present=True, + _sudo=True +) + +# Install kube packages +packages = [ + f"kubelet={version_string}-*", + f"kubeadm={version_string}-*", + f"kubectl={version_string}-*" +] + +apt.packages( + name="Install kube packages (kubeadm, kubelet, kubectl)", + packages=packages, + present=True, + update=True, + _sudo=True +) + +# Hold kube packages +for pkg in ["kubelet", "kubeadm", "kubectl"]: + pkg_status = host.get_fact(Command, f"dpkg-query -W -f='${{Status}}' {pkg} || true") + if "hold" not in (pkg_status or ""): + server.shell( + name=f"Hold {pkg}", + commands=[f"apt-mark hold {pkg}"], + _sudo=True + ) + + diff --git a/deploy/pyinfra/tasks/k8s_cluster/kubernetes_master.py b/deploy/pyinfra/tasks/k8s_cluster/kubernetes_master.py new file mode 100644 index 0000000000000000000000000000000000000000..b701b1a28bcd3b8c0f2b2e3627251604a83083af --- /dev/null +++ b/deploy/pyinfra/tasks/k8s_cluster/kubernetes_master.py @@ -0,0 +1,53 @@ +from pyinfra import host +from pyinfra.operations import server, files +from lib.operations import kubernetes + +pod_network_cidr = host.data.get('pod_network_cidr') +target_user = host.data.get('target_user') +target_home = host.data.get('target_home') + +# If it's a first run, init kubernetes +kubernetes.init_control_plane( + name="Initialize Kubernetes control plane", + pod_network_cidr=pod_network_cidr, + _sudo=True +) + +# Create .kube directory for user +files.directory( + name="Create .kube directory for user", + path=f"{target_home}/.kube", + mode="0700", + present=True +) + +# Copy admin.conf to user kubeconfig +server.shell( + name="Copy admin.conf to user kubeconfig", + commands=[ + f"cp /etc/kubernetes/admin.conf {target_home}/.kube/config", + f"chown {target_user}:{target_user} {target_home}/.kube/config", + f"chmod 0600 {target_home}/.kube/config" + ], + _sudo=True +) + +from pyinfra.facts.files import File + +# Optimization: Prevent token sprawl and redundant join command generation. +# Why: On standalone control planes without worker nodes, generating kubeadm join tokens +# creates unnecessary cluster secrets and temporary files on the local executor. +if "k8s_workers" in host.groups and len(host.groups["k8s_workers"]) > 0: + if not host.get_fact(File, path="/tmp/kubeadm_join.sh"): + server.shell( + name="Get kubeadm join command", + commands=["kubeadm token create --print-join-command > /tmp/kubeadm_join.sh"], + _sudo=True + ) + + files.download( + name="Fetch join command to local", + src="/tmp/kubeadm_join.sh", + dest="/tmp/kubeadm_join.sh", + _sudo=True + ) diff --git a/deploy/pyinfra/tasks/k8s_cluster/kubernetes_worker.py b/deploy/pyinfra/tasks/k8s_cluster/kubernetes_worker.py new file mode 100644 index 0000000000000000000000000000000000000000..867435b6f920296e6a9473e564de81c264a6e688 --- /dev/null +++ b/deploy/pyinfra/tasks/k8s_cluster/kubernetes_worker.py @@ -0,0 +1,19 @@ +from pyinfra import host +from pyinfra.operations import server, files +from lib.operations import kubernetes + +# Upload join command to worker +files.put( + name="Upload join command to worker", + src="/tmp/kubeadm_join.sh", + dest="/tmp/kubeadm_join.sh", + mode="0755", + _sudo=True +) + +# Join the cluster +kubernetes.join_cluster( + name="Join worker to cluster", + join_command_path="/tmp/kubeadm_join.sh", + _sudo=True +) diff --git a/deploy/pyinfra/tasks/system/common.py b/deploy/pyinfra/tasks/system/common.py new file mode 100644 index 0000000000000000000000000000000000000000..0edc9eb4da7cbf8dd72668b52cf48c08984f1c13 --- /dev/null +++ b/deploy/pyinfra/tasks/system/common.py @@ -0,0 +1,63 @@ +from pyinfra import host +from pyinfra.operations import apt, systemd, files, server + +apt_base_packages = host.data.get('apt_base_packages', []) + +# Recover from any previously interrupted dpkg operations +server.shell( + name="Ensure dpkg is in a clean configured state", + commands=["dpkg --configure -a || true"], + _sudo=True +) + +# Remove any conflicting/stale Docker or Kubernetes apt sources and keys before initial apt update +server.shell( + name="Remove stale or conflicting repository files before apt update", + commands=[ + "rm -f /etc/apt/sources.list.d/*docker* /etc/apt/sources.list.d/*kubernetes* /etc/apt/keyrings/docker.gpg /etc/apt/keyrings/docker.asc /usr/share/keyrings/docker*.gpg", + ], + _sudo=True, +) + +files.line( + name="Remove docker from sources.list", + path="/etc/apt/sources.list", + line=r".*download\.docker\.com.*", + present=False, + _sudo=True, +) + +files.line( + name="Remove kubernetes from sources.list", + path="/etc/apt/sources.list", + line=r".*pkgs\.k8s\.io.*", + present=False, + _sudo=True, +) + +# Update apt cache and install base packages +apt.packages( + name="Update apt cache and install base packages", + packages=apt_base_packages, + update=True, + _sudo=True +) + + +# Stop unattended-upgrades temporarily (to avoid apt lock) +systemd.service( + name="Stop unattended-upgrades temporarily", + service="unattended-upgrades", + running=False, + _sudo=True, + _ignore_errors=True # Ansible had 'failed_when: not-found not in result', pyinfra ignores errors here for simplicity if not found. +) + +# Ensure /etc/apt/keyrings exists +files.directory( + name="Ensure /etc/apt/keyrings exists", + path="/etc/apt/keyrings", + mode="0755", + present=True, + _sudo=True +) diff --git a/deploy/pyinfra/tasks/system/kernel.py b/deploy/pyinfra/tasks/system/kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..5af1aa4d1ceb95ab5a163a9b596f07372cd9c80e --- /dev/null +++ b/deploy/pyinfra/tasks/system/kernel.py @@ -0,0 +1,85 @@ +from pyinfra import host +from pyinfra.operations import files, server, systemd +from pyinfra.facts.server import Command + +disable_swap = host.data.get('disable_swap', True) + +if disable_swap: + # Disable swap at runtime if enabled. + # We check if there's any swap configured first. + swap_total = host.get_fact(Command, "free -m | awk '/Swap/ {print $2}'") + + if swap_total and swap_total.strip() != "0": + server.shell( + name="Disable swap at runtime if enabled", + commands=["swapoff -a"], + _sudo=True + ) + + # Comment out any active swap entries in fstab + files.replace( + name="Comment out any active swap entries in fstab", + path="/etc/fstab", + text=r'^([^#].*\s+swap\s+.*)$', + replace=r'# \1', + _sudo=True + ) + +# Ensure kernel modules are present +modules = ["overlay", "br_netfilter"] +for mod in modules: + server.modprobe( + name=f"Ensure kernel module {mod} is present", + module=mod, + present=True, + _sudo=True + ) + +# Persist kernel modules +files.template( + name="Persist kernel modules", + src="templates/k8s.conf.j2", # We will create a template or just write a file + dest="/etc/modules-load.d/k8s.conf", + mode="0644", + _sudo=True +) + + +# Configure sysctl for Kubernetes networking +sysctl_vars = [ + ("net.bridge.bridge-nf-call-iptables", 1), + ("net.bridge.bridge-nf-call-ip6tables", 1), + ("net.ipv4.ip_forward", 1), + ("net.ipv6.conf.all.disable_ipv6", 1), + ("net.ipv6.conf.default.disable_ipv6", 1), +] + +for name, value in sysctl_vars: + server.sysctl( + name=f"Configure sysctl {name}", + key=name, + value=value, + persist=True, + _sudo=True + ) + +# Reload systemd (if needed) +systemd.daemon_reload( + name="Reload systemd", + _sudo=True +) + +# Disable IPv6 at the kernel level via GRUB +files.replace( + name="Disable IPv6 in GRUB", + path="/etc/default/grub", + text=r'^GRUB_CMDLINE_LINUX="((?!.*ipv6\.disable=1).*)"$', + replace=r'GRUB_CMDLINE_LINUX="\1 ipv6.disable=1"', + _sudo=True +) + +server.shell( + name="Update GRUB", + commands=["update-grub"], + _sudo=True +) diff --git a/deploy/pyinfra/templates/k8s.conf.j2 b/deploy/pyinfra/templates/k8s.conf.j2 new file mode 100644 index 0000000000000000000000000000000000000000..43dd5433bcb78814e546eec99f7083f37e0a86a1 --- /dev/null +++ b/deploy/pyinfra/templates/k8s.conf.j2 @@ -0,0 +1,2 @@ +overlay +br_netfilter diff --git a/etsi-mec-sandbox-frontend b/etsi-mec-sandbox-frontend index 8e86af28269336ea0552508884d12203fc70cddf..db8ddff3b76de52b5b10e74f138ee2d7be073ef8 160000 --- a/etsi-mec-sandbox-frontend +++ b/etsi-mec-sandbox-frontend @@ -1 +1 @@ -Subproject commit 8e86af28269336ea0552508884d12203fc70cddf +Subproject commit db8ddff3b76de52b5b10e74f138ee2d7be073ef8 diff --git a/go-apps/meep-auth-svc/Dockerfile b/go-apps/meep-auth-svc/Dockerfile index c390b5e04d2df6ae3b8c15429f26993ac8a079f9..32191632b503b3ac3d10c1a3f5bd633dc7665dae 100644 --- a/go-apps/meep-auth-svc/Dockerfile +++ b/go-apps/meep-auth-svc/Dockerfile @@ -21,7 +21,8 @@ COPY ./data / RUN chmod +x /entrypoint.sh RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -f -y ca-certificates + && DEBIAN_FRONTEND=noninteractive apt-get install -f -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* RUN dpkg --configure -a diff --git a/go-apps/meep-auth-svc/server/auth-svc.go b/go-apps/meep-auth-svc/server/auth-svc.go index 2eba8979e28959f859c34b82412560d540b29869..3c34d7db9987379d947d16ec6dc5e3e6157be62f 100644 --- a/go-apps/meep-auth-svc/server/auth-svc.go +++ b/go-apps/meep-auth-svc/server/auth-svc.go @@ -1312,6 +1312,10 @@ func startSession(provider string, username string, w http.ResponseWriter, r *ht sandbox, _, err := authSvc.pfmCtrlClient.SandboxControlApi.CreateSandbox(ctx, sandboxConfig) cancel() if err != nil { + if strings.Contains(err.Error(), "connect: operation not permitted") || strings.Contains(err.Error(), "connection refused") { + err = errors.New("Service temporarily unavailable, please try again in 30 seconds") + return "", false, "", err, http.StatusServiceUnavailable + } return "", false, "", err, http.StatusInternalServerError } sandboxName = sandbox.Name @@ -1320,6 +1324,10 @@ func startSession(provider string, username string, w http.ResponseWriter, r *ht _, err := authSvc.pfmCtrlClient.SandboxControlApi.CreateSandboxWithName(ctx, sandboxName, sandboxConfig) cancel() if err != nil && !strings.Contains(err.Error(), "409") && !strings.Contains(err.Error(), "Conflict") { + if strings.Contains(err.Error(), "connect: operation not permitted") || strings.Contains(err.Error(), "connection refused") { + err = errors.New("Service temporarily unavailable, please try again in 30 seconds") + return "", false, "", err, http.StatusServiceUnavailable + } return "", false, "", err, http.StatusInternalServerError } } diff --git a/go-apps/meep-dai/Dockerfile b/go-apps/meep-dai/Dockerfile index f9625f6d651209e003ab40f12acf944e5553ad42..56d1d6ffd85a94bcc41bf106108a8c937be9d125 100644 --- a/go-apps/meep-dai/Dockerfile +++ b/go-apps/meep-dai/Dockerfile @@ -26,7 +26,8 @@ RUN chmod +x /entrypoint.sh RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends procps lftp nfs-common \ - && mkdir -p /mnt/nfs/mec_sandbox + && mkdir -p /mnt/nfs/mec_sandbox \ + && rm -rf /var/lib/apt/lists/* # \ # && sudo chmod -R 777 /mnt/nfs/ \ # && mount -t nfs $HOSTNAME:/mnt/nfs/mec_sandbox /mnt/nfs/mec_sandbox diff --git a/go-apps/meep-federation/Dockerfile b/go-apps/meep-federation/Dockerfile index 461f3f0771e4455e5a64734c35c585c7d3ad7f84..8c82d4d3be4d9d0afe9de5076eeeb7417b79436b 100644 --- a/go-apps/meep-federation/Dockerfile +++ b/go-apps/meep-federation/Dockerfile @@ -21,7 +21,8 @@ COPY ./data / RUN chmod +x /entrypoint.sh RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl jq + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl jq \ + && rm -rf /var/lib/apt/lists/* RUN dpkg --configure -a diff --git a/go-apps/meep-federation/entrypoint.sh b/go-apps/meep-federation/entrypoint.sh index f12a1f22c8c8e6a5a33d9bdec7998c6dbcae71dd..d65602b8b8f98a3d3dd6c5ea6f89684bd75f3efd 100755 --- a/go-apps/meep-federation/entrypoint.sh +++ b/go-apps/meep-federation/entrypoint.sh @@ -14,7 +14,9 @@ else fi WEBSOCK_ETPATH="/$MEEP_SANDBOX_NAME"${WEBSOCK_ETPATH:-"/monaco-telecom/meep-cloud-mosquitto"} -MEEP_BROKER="wss://${MEEP_HOST_URL#https://}:443$WEBSOCK_ETPATH" +TMP_URL="${MEEP_HOST_URL#https://}" +TMP_URL="${TMP_URL#http://}" +MEEP_BROKER="wss://${TMP_URL}:443$WEBSOCK_ETPATH" # Update API yaml basepaths to enable "Try-it-out" feature # OAS2: Set relative path to sandbox name + endpoint path (origin will be derived from browser URL) diff --git a/go-apps/meep-iot-pltf/meep-acme-in-cse/Dockerfile b/go-apps/meep-iot-pltf/meep-acme-in-cse/Dockerfile index 44720f8b5e7b646b456136e0ac2824405fed0a20..346f71e4257aabbb8a2ac207f725d0712ec70179 100644 --- a/go-apps/meep-iot-pltf/meep-acme-in-cse/Dockerfile +++ b/go-apps/meep-iot-pltf/meep-acme-in-cse/Dockerfile @@ -1,29 +1,29 @@ FROM python:3.11-slim RUN DEBIAN_FRONTEND=noninteractive apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - curl \ - gettext \ - git \ - gnutls-bin \ - iputils-ping \ - jq \ - libedit2 \ - libffi-dev \ - libglib2.0-dev \ - libssl-dev \ - lsof \ - pkg-config \ - sudo \ - tzdata \ - && DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y \ - && DEBIAN_FRONTEND=noninteractive apt-get autoclean \ - && DEBIAN_FRONTEND=noninteractive apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + curl \ + gettext \ + git \ + gnutls-bin \ + iputils-ping \ + jq \ + libedit2 \ + libffi-dev \ + libglib2.0-dev \ + libssl-dev \ + lsof \ + pkg-config \ + sudo \ + tzdata \ + && DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y \ + && DEBIAN_FRONTEND=noninteractive apt-get autoclean \ + && DEBIAN_FRONTEND=noninteractive apt-get clean \ + && rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/app -RUN git clone https://github.com/ankraft/ACME-oneM2M-CSE.git ACME-oneM2M-CSE +RUN git clone -b development https://github.com/ankraft/ACME-oneM2M-CSE.git ACME-oneM2M-CSE WORKDIR /usr/src/app/ACME-oneM2M-CSE @@ -50,6 +50,8 @@ RUN pip3 install --no-cache-dir -r requirements.txt --break-system-packages COPY ./data /usr/src/app/ACME-oneM2M-CSE +RUN cp -r ./acme/* ./acmecse/ && rm -rf ./acme || true + RUN chmod +x entrypoint.sh ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/go-apps/meep-iot-pltf/meep-acme-in-cse/entrypoint.sh b/go-apps/meep-iot-pltf/meep-acme-in-cse/entrypoint.sh index 96ef474aa1338185cf57836d8bab6f13d33657a3..9463b2832764a280fb5e8e18fecb14a6a5b26fd7 100755 --- a/go-apps/meep-iot-pltf/meep-acme-in-cse/entrypoint.sh +++ b/go-apps/meep-iot-pltf/meep-acme-in-cse/entrypoint.sh @@ -111,4 +111,4 @@ workdir="/usr/src/app/ACME-oneM2M-CSE" cd "$workdir" || { echo "Directory $workdir not found"; exit 1; } envsubst < acme.ini.in > acme.ini cat acme.ini -python3 -m acmecse +exec python3 -m acmecse diff --git a/go-apps/meep-iot-pltf/meep-acme-mn-cse/Dockerfile b/go-apps/meep-iot-pltf/meep-acme-mn-cse/Dockerfile index 2c3dfb8d87a7ec363515c98e42d5f354a781ea5b..b9adc25684ea77f03a1a669d35bcde594bda1b11 100644 --- a/go-apps/meep-iot-pltf/meep-acme-mn-cse/Dockerfile +++ b/go-apps/meep-iot-pltf/meep-acme-mn-cse/Dockerfile @@ -1,29 +1,29 @@ FROM python:3.11-slim RUN DEBIAN_FRONTEND=noninteractive apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - curl \ - gettext \ - git \ - gnutls-bin \ - iputils-ping \ - jq \ - libedit2 \ - libffi-dev \ - libglib2.0-dev \ - libssl-dev \ - lsof \ - pkg-config \ - sudo \ - tzdata \ - && DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y \ - && DEBIAN_FRONTEND=noninteractive apt-get autoclean \ - && DEBIAN_FRONTEND=noninteractive apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + curl \ + gettext \ + git \ + gnutls-bin \ + iputils-ping \ + jq \ + libedit2 \ + libffi-dev \ + libglib2.0-dev \ + libssl-dev \ + lsof \ + pkg-config \ + sudo \ + tzdata \ + && DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y \ + && DEBIAN_FRONTEND=noninteractive apt-get autoclean \ + && DEBIAN_FRONTEND=noninteractive apt-get clean \ + && rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/app -RUN git clone https://github.com/ankraft/ACME-oneM2M-CSE.git ACME-oneM2M-CSE +RUN git clone -b development https://github.com/ankraft/ACME-oneM2M-CSE.git ACME-oneM2M-CSE WORKDIR /usr/src/app/ACME-oneM2M-CSE diff --git a/go-apps/meep-iot-pltf/meep-acme-mn-cse/acmecse/plugins/services/MECSupport.py b/go-apps/meep-iot-pltf/meep-acme-mn-cse/acmecse/plugins/services/MECSupport.py index f8092622ab451ebbe3de25ba05d4d924ed25f503..ae09b7447d5100d6aab09f660a0bc3c03c54163f 100644 --- a/go-apps/meep-iot-pltf/meep-acme-mn-cse/acmecse/plugins/services/MECSupport.py +++ b/go-apps/meep-iot-pltf/meep-acme-mn-cse/acmecse/plugins/services/MECSupport.py @@ -27,6 +27,7 @@ from acmecse.etc.ResponseStatusCodes import ResponseException from acmecse.runtime.Configuration import Configuration from acmecse.etc.Types import Result, ResponseStatusCode from acmecse.runtime.Configuration import Configuration, ConfigurationError +from acmecse.runtime.EventManager import onEvent, EventData, eventManager, eventHandler import requests import isodate @@ -38,6 +39,7 @@ import uuid import json import threading +@eventHandler @plugin(property='MECSupport', tags=['acme', 'core']) @requires(cseShutdown='acmecse.runtime.CSE.shutdown') class MECSupport: @@ -104,17 +106,25 @@ class MECSupport: self.thread.start() L.isInfo and L.log('startMECSupport: MEC Flask app running in background thread') self.isStopped = False - # Register to MEC platform - self.registerToMECPlatform() - L.isInfo and L.log('startMECSupport: Starting MEC registration worker') - self.mecRegistrationWorker = BackgroundWorkerPool.newWorker(20, # Don't care, the worker will be stopped by registerAsIoTPlatform() in any case - self.registerAsIoTPlatform, - 'MECSupport').start() except Exception as e: L.logErr(f'startMECSupport: failed to start MEC Flask thread: {e}') L.isInfo and L.log('<<< startMECSupport') return + @onEvent(eventManager.cseStartup) + def startMECRegistration(self, eventData: EventData) -> None: + L.isDebug and L.logDebug('startMECRegistration: Initiating MN-CSE Registration to MEC on cseStartup event') + if not Configuration.mec_enable or self.isStopped: + L.isInfo and L.log('MEC Client disabled or stopped') + return + # Register to MEC platform + self.registerToMECPlatform() + L.isInfo and L.log('startMECRegistration: Starting MEC registration worker') + self.mecRegistrationWorker = BackgroundWorkerPool.newWorker(20, # Don't care, the worker will be stopped by registerAsIoTPlatform() in any case + self.registerAsIoTPlatform, + 'MECSupport').start() + return + @finish def finishMECSupport(self) -> None: L.isDebug and L.logDebug('Finishing MN-CSE Registration to MEC plugin') diff --git a/go-apps/meep-iot-pltf/meep-acme-mn-cse/acmecse/services/RequestManager.py b/go-apps/meep-iot-pltf/meep-acme-mn-cse/acmecse/services/RequestManager.py index 74ec508e6628b2865c48a8b23ed6d16c25ed0dec..a9c3916238ce6292e62ff2bf9a6714d9432046b4 100644 --- a/go-apps/meep-iot-pltf/meep-acme-mn-cse/acmecse/services/RequestManager.py +++ b/go-apps/meep-iot-pltf/meep-acme-mn-cse/acmecse/services/RequestManager.py @@ -1343,7 +1343,14 @@ class RequestManager(metaclass=Singleton): # raise BAD_REQUEST(L.logDebug('from/originator parameter is mandatory in request'), data = cseRequest) # else: # cseRequest.originator = fr - cseRequest.originator = cseRequest.originalOriginator = gget(cseRequest.originalRequest, 'fr', greedy=False) + if cseRequest.originalRequest: + cseRequest.originator = cseRequest.originalOriginator = cseRequest.originalRequest.get('fr') + else: + cseRequest.originator = cseRequest.originalOriginator = None + if cseRequest.originator: + # Remove it from the dictionary if present, since gget with greedy=True would do this + cseRequest.originalRequest.pop('fr', None) + self._originatorAdaptToScope(cseRequest, True) # Convert "from" to CSE-relative format if possible # RQI - requestIdentifier diff --git a/go-apps/meep-iot-pltf/meep-acme-mn-cse/entrypoint.sh b/go-apps/meep-iot-pltf/meep-acme-mn-cse/entrypoint.sh index 1761c021204c39c22fcce8c4019f9e7befa083f4..e6591d3849853dc9498e803f6285a8f33dd7534a 100755 --- a/go-apps/meep-iot-pltf/meep-acme-mn-cse/entrypoint.sh +++ b/go-apps/meep-iot-pltf/meep-acme-mn-cse/entrypoint.sh @@ -205,4 +205,4 @@ workdir="/usr/src/app/ACME-oneM2M-CSE" cd "$workdir" || { echo "Directory $workdir not found"; exit 1; } envsubst < acme.ini.in > acme.ini cat acme.ini -python3 -m acmecse +exec python3 -m acmecse diff --git a/go-apps/meep-iot-pltf/tinyiot-in-cse/entrypoint.sh b/go-apps/meep-iot-pltf/tinyiot-in-cse/entrypoint.sh index d18488fb4500a88d054565cae5d3eadac6dced0b..664ca6ee25d15fee86576058805207eb9162db11 100755 --- a/go-apps/meep-iot-pltf/tinyiot-in-cse/entrypoint.sh +++ b/go-apps/meep-iot-pltf/tinyiot-in-cse/entrypoint.sh @@ -168,4 +168,12 @@ fi # Wait for Server Process # ------------------------------------------------------------------ +trap_term() { + echo "Caught SIGTERM signal!" + kill -TERM "$SERVER_PID" 2>/dev/null + wait "$SERVER_PID" + exit 0 +} +trap trap_term SIGTERM SIGINT + wait $SERVER_PID \ No newline at end of file diff --git a/go-apps/meep-iot-pltf/tinyiot-mn-cse/entrypoint.sh b/go-apps/meep-iot-pltf/tinyiot-mn-cse/entrypoint.sh index d5e1e863b6e05b2175c7f0817d7ed2f53ee32c10..448e09f24ae603453207c7d2d39aa81eadb75ff7 100755 --- a/go-apps/meep-iot-pltf/tinyiot-mn-cse/entrypoint.sh +++ b/go-apps/meep-iot-pltf/tinyiot-mn-cse/entrypoint.sh @@ -206,4 +206,12 @@ fi # ------------------------------------------------------------------ # Wait for Server Process # ------------------------------------------------------------------ +trap_term() { + echo "Caught SIGTERM signal!" + kill -TERM "$SERVER_PID" 2>/dev/null + wait "$SERVER_PID" + exit 0 +} +trap trap_term SIGTERM SIGINT + wait $SERVER_PID \ No newline at end of file diff --git a/go-apps/meep-iot/MEC_033_046_LCM_Testing.postman_collection.json b/go-apps/meep-iot/MEC_033_046_LCM_Testing.postman_collection.json new file mode 100644 index 0000000000000000000000000000000000000000..eb9895f1c1e0d68995cefcf8628f5ff7a4e728de --- /dev/null +++ b/go-apps/meep-iot/MEC_033_046_LCM_Testing.postman_collection.json @@ -0,0 +1,654 @@ +{ + "info": { + "name": "MEC 033 & 046 LCM Testing", + "description": "Comprehensive LCM Testing for IoT API (MEC 033) and Sensor Sharing Service API (MEC 046).", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "base_url", + "value": "http://10.98.111.115/iots/v1", + "type": "string" + }, + { + "key": "sss_base_url", + "value": "http://10.110.109.193/sens/v1", + "type": "string" + }, + { + "key": "iotPlatformId", + "value": "", + "type": "string" + }, + { + "key": "deviceId", + "value": "", + "type": "string" + }, + { + "key": "subscriptionId", + "value": "", + "type": "string" + } + ], + "item": [ + { + "name": "MEC 033 - IoT API", + "item": [ + { + "name": "1. POST /registered_iot_platforms", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = pm.response.json();", + "if(jsonData.iotPlatformId) {", + " pm.environment.set('iotPlatformId', jsonData.iotPlatformId);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"iotPlatformId\": \"my-postman-iot-platform\",\n \"userTransportInfo\": [\n {\n \"id\": \"mqtt-01\",\n \"name\": \"my-mqtt\",\n \"type\": \"MB_TOPIC_BASED\",\n \"protocol\": \"MQTT\",\n \"version\": \"3.1.1\",\n \"endpoint\": { \"addresses\": [ { \"host\": \"192.168.20.167\", \"port\": 1883 } ] },\n \"security\": {},\n \"implSpecificInfo\": {}\n }\n ],\n \"customServicesTransportInfo\": [],\n \"enabled\": true\n}" + }, + "url": { + "raw": "{{base_url}}/registered_iot_platforms", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_iot_platforms" + ] + } + } + }, + { + "name": "2. GET /registered_iot_platforms", + "request": { + "method": "GET", + "url": { + "raw": "{{base_url}}/registered_iot_platforms", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_iot_platforms" + ] + } + } + }, + { + "name": "3. GET /registered_iot_platforms/{iotPlatformId}", + "request": { + "method": "GET", + "url": { + "raw": "{{base_url}}/registered_iot_platforms/{{iotPlatformId}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_iot_platforms", + "{{iotPlatformId}}" + ] + } + } + }, + { + "name": "4. PUT /registered_iot_platforms/{iotPlatformId}", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"iotPlatformId\": \"{{iotPlatformId}}\",\n \"userTransportInfo\": [],\n \"customServicesTransportInfo\": [],\n \"enabled\": false\n}" + }, + "url": { + "raw": "{{base_url}}/registered_iot_platforms/{{iotPlatformId}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_iot_platforms", + "{{iotPlatformId}}" + ] + } + } + }, + { + "name": "5. DELETE /registered_iot_platforms/{iotPlatformId}", + "request": { + "method": "DELETE", + "url": { + "raw": "{{base_url}}/registered_iot_platforms/{{iotPlatformId}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_iot_platforms", + "{{iotPlatformId}}" + ] + } + } + }, + { + "name": "6. POST /registered_devices", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = pm.response.json();", + "if(jsonData.deviceId) {", + " pm.environment.set('deviceId', jsonData.deviceId);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"deviceId\": \"my-postman-device\",\n \"requestedIotPlatformId\": \"{{iotPlatformId}}\",\n \"deviceAuthenticationInfo\": \"auth-123\",\n \"enabled\": true\n}" + }, + "url": { + "raw": "{{base_url}}/registered_devices", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_devices" + ] + } + } + }, + { + "name": "7. GET /registered_devices/{deviceId}", + "request": { + "method": "GET", + "url": { + "raw": "{{base_url}}/registered_devices/{{deviceId}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_devices", + "{{deviceId}}" + ] + } + } + }, + { + "name": "7.5. PUT /registered_devices/{deviceId}", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"deviceId\": \"{{deviceId}}\",\n \"requestedIotPlatformId\": \"{{iotPlatformId}}\",\n \"deviceAuthenticationInfo\": \"auth-123-updated\",\n \"enabled\": false\n}" + }, + "url": { + "raw": "{{base_url}}/registered_devices/{{deviceId}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_devices", + "{{deviceId}}" + ] + } + } + }, + { + "name": "8. DELETE /registered_devices/{deviceId}", + "request": { + "method": "DELETE", + "url": { + "raw": "{{base_url}}/registered_devices/{{deviceId}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "registered_devices", + "{{deviceId}}" + ] + } + } + } + ] + }, + { + "name": "MEC 046 - SSS API", + "item": [ + { + "name": "1. POST /subscriptions/sensor_discovery", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = pm.response.json();", + "if(jsonData.subscriptionId) {", + " pm.environment.set('subscriptionId', jsonData.subscriptionId);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"callbackReference\": \"http://example.com/callback\",\n \"sensorType\": [\"AE\"]\n}" + }, + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_discovery", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_discovery" + ] + } + } + }, + { + "name": "2. GET /subscriptions/sensor_discovery", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_discovery", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_discovery" + ] + } + } + }, + { + "name": "3. GET /subscriptions/sensor_discovery/{subscriptionId}", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_discovery/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_discovery", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "4. PUT /subscriptions/sensor_discovery/{subscriptionId}", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"subscriptionId\": \"{{subscriptionId}}\",\n \"callbackReference\": \"http://example.com/callback_updated\",\n \"sensorType\": [\"AE\", \"CNT\"]\n}" + }, + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_discovery/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_discovery", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "5. DELETE /subscriptions/sensor_discovery/{subscriptionId}", + "request": { + "method": "DELETE", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_discovery/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_discovery", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "6. GET /queries/sensor_discovery", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/queries/sensor_discovery", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "queries", + "sensor_discovery" + ] + } + } + }, + { + "name": "7. POST /subscriptions/sensor_status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = pm.response.json();", + "if(jsonData.subscriptionId) {", + " pm.environment.set('subscriptionId', jsonData.subscriptionId);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"callbackReference\": \"http://example.com/status\",\n \"sensorId\": \"sensor-123\"\n}" + }, + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_status", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_status" + ] + } + } + }, + { + "name": "8. GET /subscriptions/sensor_status", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_status", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_status" + ] + } + } + }, + { + "name": "9. GET /subscriptions/sensor_status/{subscriptionId}", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_status/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_status", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "10. PUT /subscriptions/sensor_status/{subscriptionId}", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"subscriptionId\": \"{{subscriptionId}}\",\n \"callbackReference\": \"http://example.com/status_updated\",\n \"sensorId\": \"sensor-123\"\n}" + }, + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_status/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_status", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "11. DELETE /subscriptions/sensor_status/{subscriptionId}", + "request": { + "method": "DELETE", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_status/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_status", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "12. GET /queries/sensor_status", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/queries/sensor_status", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "queries", + "sensor_status" + ] + } + } + }, + { + "name": "13. POST /subscriptions/sensor_data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = pm.response.json();", + "if(jsonData.subscriptionId) {", + " pm.environment.set('subscriptionId', jsonData.subscriptionId);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"callbackReference\": \"http://example.com/data\",\n \"sensorId\": \"sensor-123\"\n}" + }, + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_data", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_data" + ] + } + } + }, + { + "name": "14. GET /subscriptions/sensor_data", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_data", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_data" + ] + } + } + }, + { + "name": "15. GET /subscriptions/sensor_data/{subscriptionId}", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_data/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_data", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "16. PUT /subscriptions/sensor_data/{subscriptionId}", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"subscriptionId\": \"{{subscriptionId}}\",\n \"callbackReference\": \"http://example.com/data_updated\",\n \"sensorId\": \"sensor-123\"\n}" + }, + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_data/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_data", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "17. DELETE /subscriptions/sensor_data/{subscriptionId}", + "request": { + "method": "DELETE", + "url": { + "raw": "{{sss_base_url}}/subscriptions/sensor_data/{{subscriptionId}}", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "subscriptions", + "sensor_data", + "{{subscriptionId}}" + ] + } + } + }, + { + "name": "18. GET /queries/sensor_data", + "request": { + "method": "GET", + "url": { + "raw": "{{sss_base_url}}/queries/sensor_data", + "host": [ + "{{sss_base_url}}" + ], + "path": [ + "queries", + "sensor_data" + ] + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/go-apps/meep-sandbox-ctrl/server/app-ctrl.go b/go-apps/meep-sandbox-ctrl/server/app-ctrl.go index 7b0502ae16ec034f6964ef59338d07ef9449c3b9..ead7b625fb0bd4c756ace3e9119634b557cde71c 100644 --- a/go-apps/meep-sandbox-ctrl/server/app-ctrl.go +++ b/go-apps/meep-sandbox-ctrl/server/app-ctrl.go @@ -777,6 +777,7 @@ func stopMECFederationDiscovery() { log.Debug("stopMECFederationDiscovery: Stopping MEC Federation ticker") fed_timer.Stop() fed_timer = nil + fed_timer_count = 0 fed_list = make(map[string]bool) log.Debug("stopMECFederationDiscovery: len(fed_list)=", len(fed_list)) } diff --git a/go-apps/meep-tc-engine/routing-engine.go b/go-apps/meep-tc-engine/routing-engine.go index 1ea5dd0218ed7d52c7735d2981c716a883c0f383..0b619c001d252af273b3de2a300289e3cd7caa00 100644 --- a/go-apps/meep-tc-engine/routing-engine.go +++ b/go-apps/meep-tc-engine/routing-engine.go @@ -83,32 +83,30 @@ func (re *RoutingEngine) RefreshLbRules() { // Retrieve LB rules from DB jsonNetElemList, err := re.lbRulesStore.rc.JSONGetEntry(re.lbRulesStore.baseKey+typeLb, ".") if err != nil { - log.Error(err.Error()) - return - } - - // Unmarshal MG Service Maps - var netElemList mgModel.NetworkElementList - err = json.Unmarshal([]byte(jsonNetElemList), &netElemList) - if err != nil { - log.Error(err.Error()) - return - } - - // Update pod MG service mappings - for _, netElem := range netElemList.NetworkElements { - podInfo := podInfoMap[netElem.Name] - if podInfo == nil { - log.Error("Failed to find network element: ", netElem.Name) - continue - } + log.Debug("No MG LB rules found: ", err.Error()) + } else { + // Unmarshal MG Service Maps + var netElemList mgModel.NetworkElementList + err = json.Unmarshal([]byte(jsonNetElemList), &netElemList) + if err != nil { + log.Error(err.Error()) + } else { + // Update pod MG service mappings + for _, netElem := range netElemList.NetworkElements { + podInfo := podInfoMap[netElem.Name] + if podInfo == nil { + log.Error("Failed to find network element: ", netElem.Name) + continue + } - // Set load balanced MG Service instance - for _, svcMap := range netElem.ServiceMaps { - if svcInfo, found := svcInfoMap[svcMap.LbSvcName]; found { - podInfo.MgSvcMap[svcMap.MgSvcName] = svcInfo - } else { - log.Error("failed to find service instance: ", svcMap.LbSvcName) + // Set load balanced MG Service instance + for _, svcMap := range netElem.ServiceMaps { + if svcInfo, found := svcInfoMap[svcMap.LbSvcName]; found { + podInfo.MgSvcMap[svcMap.MgSvcName] = svcInfo + } else { + log.Error("failed to find service instance: ", svcMap.LbSvcName) + } + } } } } diff --git a/go-apps/meep-tc-sidecar/Dockerfile b/go-apps/meep-tc-sidecar/Dockerfile index 298f80daf0807ac4ea33145bd7dd7248de01b09b..f445e9871f63ca9df2d37d47df7e780d44505c23 100644 --- a/go-apps/meep-tc-sidecar/Dockerfile +++ b/go-apps/meep-tc-sidecar/Dockerfile @@ -20,6 +20,6 @@ COPY ./data / RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.list -RUN apt-get update && apt-get install -y iputils-ping iproute2 iptables conntrack net-tools +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends iputils-ping iproute2 iptables conntrack net-tools curl && rm -rf /var/lib/apt/lists/* ENTRYPOINT ["/meep-tc-sidecar"] diff --git a/go-apps/meep-tc-sidecar/main.go b/go-apps/meep-tc-sidecar/main.go index e2478e214babb31500b960276ed099f39ec6083d..9a69bac7de7ac3949b3f65a4356cf8a731147b88 100644 --- a/go-apps/meep-tc-sidecar/main.go +++ b/go-apps/meep-tc-sidecar/main.go @@ -58,9 +58,13 @@ const svcPrefix string = "SVC-" const mePrefix string = meepPrefix + "ME-" const ingressPrefix string = meepPrefix + "INGRESS-" const egressPrefix string = meepPrefix + "EGRESS-" +const egressSnatPrefix string = meepPrefix + "E-SNAT-" +const ingressSnatPrefix string = meepPrefix + "I-SNAT-" const meSvcChain string = mePrefix + "SERVICES" const ingressSvcChain string = ingressPrefix + "SERVICES" const egressSvcChain string = egressPrefix + "SERVICES" +const egressSnatChain string = egressSnatPrefix + "SERVICES" +const ingressSnatChain string = ingressSnatPrefix + "SERVICES" const maxChainLen int = 25 const capLetters string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" const ipAddrNone string = "n/a" @@ -358,13 +362,6 @@ func refreshLbRules() { } } - // Reapply masquerading rule if not present - err = ipTbl.AppendUnique("nat", "POSTROUTING", "-o", "eth0", "-j", "MASQUERADE") - if err != nil { - log.Error("Failed to set rule [-A POSTROUTING -o eth0 -j MASQUERADE]. Error: ", err) - return - } - // Create top-level MEEP service chains if not present // MEEP-ME-SERVICES _, exists := chainMap[meSvcChain] @@ -402,6 +399,30 @@ func refreshLbRules() { } delete(chainMap, egressSvcChain) + // MEEP-E-SNAT-SERVICES + _, exists = chainMap[egressSnatChain] + if !exists { + log.Debug("Creating MEEP chain MEEP-E-SNAT-SERVICES") + err = ipTbl.NewChain("nat", egressSnatChain) + if err != nil { + log.Error("Failed to create chain. Error: ", err) + return + } + } + delete(chainMap, egressSnatChain) + + // MEEP-I-SNAT-SERVICES + _, exists = chainMap[ingressSnatChain] + if !exists { + log.Debug("Creating MEEP chain MEEP-I-SNAT-SERVICES") + err = ipTbl.NewChain("nat", ingressSnatChain) + if err != nil { + log.Error("Failed to create chain. Error: ", err) + return + } + } + delete(chainMap, ingressSnatChain) + // Reapply top-level routing rules if not present err = ipTbl.AppendUnique("nat", "OUTPUT", "-j", meSvcChain) if err != nil { @@ -418,6 +439,16 @@ func refreshLbRules() { log.Error("Failed to set rule [-A PREROUTING -j "+egressSvcChain+"]. Error: ", err) return } + err = ipTbl.AppendUnique("nat", "POSTROUTING", "-o", "eth0", "-j", egressSnatChain) + if err != nil { + log.Error("Failed to set rule [-A POSTROUTING -o eth0 -j "+egressSnatChain+"]. Error: ", err) + return + } + err = ipTbl.AppendUnique("nat", "POSTROUTING", "-o", "eth0", "-j", ingressSnatChain) + if err != nil { + log.Error("Failed to set rule [-A POSTROUTING -o eth0 -j "+ingressSnatChain+"]. Error: ", err) + return + } // Apply pod-specific LB rules stored in DB flushRequired = false @@ -435,6 +466,10 @@ func refreshLbRules() { if strings.Contains(chain, ingressPrefix) { parentChain = ingressSvcChain + } else if strings.Contains(chain, egressSnatPrefix) { + parentChain = egressSnatChain + } else if strings.Contains(chain, ingressSnatPrefix) { + parentChain = ingressSnatChain } else if strings.Contains(chain, egressPrefix) { parentChain = egressSvcChain } else { @@ -543,6 +578,17 @@ func refreshLbRulesHandler(key string, fields map[string]string, userData interf // No update required. Remove chain from chain map and return. if exists { delete(*chainMap, serviceChain) + if fields[fieldSvcType] == typeEgressSvc { + err = addEgressSnatRule(fields, chainMap) + if err != nil { + return err + } + } else if fields[fieldSvcType] == typeIngressSvc { + err = addIngressSnatRule(fields, chainMap) + if err != nil { + return err + } + } return nil } } @@ -574,6 +620,135 @@ func refreshLbRulesHandler(key string, fields map[string]string, userData interf return err } + // For Egress services, also create destination-scoped SNAT/MASQUERADE rule in MEEP-E-SNAT-SERVICES + if fields[fieldSvcType] == typeEgressSvc { + err = addEgressSnatRule(fields, chainMap) + if err != nil { + return err + } + } else if fields[fieldSvcType] == typeIngressSvc { + err = addIngressSnatRule(fields, chainMap) + if err != nil { + return err + } + } + + flushRequired = true + return nil +} + +func addEgressSnatRule(fields map[string]string, chainMap *map[string]bool) error { + var err error + servicePrefix := egressSnatPrefix + svcPrefix + service := servicePrefix + strings.ToUpper(fields[fieldSvcName]) + "-" + fields[fieldSvcPort] + var args []string + args = append(args, "-p", fields[fieldSvcProtocol], "-d", fields[fieldLbSvcIp], "--dport", fields[fieldLbSvcPort], + "-j", "MASQUERADE", "-m", "comment", "--comment", service) + + // Retrieve service chain name if service exists + serviceChain, exists := serviceChains[service] + if exists { + // Check if chain exists + _, exists = (*chainMap)[serviceChain] + if exists { + // Check if rule requires update + exists, err = ipTbl.Exists("nat", serviceChain, args...) + if err != nil { + log.Error("Failed to check if rule exists. Error: ", err) + return err + } + + // No update required. Remove chain from chain map and return. + if exists { + delete(*chainMap, serviceChain) + return nil + } + } + } + + // Create new service chain name + log.Debug("Creating new service chain mapping for SNAT service: ", service) + serviceChain = servicePrefix + randSeq(maxChainLen-len(servicePrefix)) + serviceChains[service] = serviceChain + + // Create MEEP service chain + log.Debug("Creating MEEP chain ", serviceChain) + err = ipTbl.NewChain("nat", serviceChain) + if err != nil { + log.Error("Failed to create chain. Error: ", err) + return err + } + + // Create service routing rules + err = ipTbl.AppendUnique("nat", egressSnatChain, "-j", serviceChain) + if err != nil { + log.Error("Failed to set rule [-A ", egressSnatChain, " -j ", serviceChain, "]. Error: ", err) + return err + } + err = ipTbl.AppendUnique("nat", serviceChain, args...) + if err != nil { + log.Error("Failed to set rule [-A ", egressSnatChain, " -j ", serviceChain, " ", args, "]. Error: ", err) + return err + } + + flushRequired = true + return nil +} + +func addIngressSnatRule(fields map[string]string, chainMap *map[string]bool) error { + var err error + servicePrefix := ingressSnatPrefix + svcPrefix + service := servicePrefix + strings.ToUpper(fields[fieldSvcName]) + "-" + fields[fieldSvcPort] + var args []string + args = append(args, "-p", fields[fieldSvcProtocol], "-d", fields[fieldLbSvcIp], "--dport", fields[fieldLbSvcPort], + "-j", "MASQUERADE", "-m", "comment", "--comment", service) + + // Retrieve service chain name if service exists + serviceChain, exists := serviceChains[service] + if exists { + // Check if chain exists + _, exists = (*chainMap)[serviceChain] + if exists { + // Check if rule requires update + exists, err = ipTbl.Exists("nat", serviceChain, args...) + if err != nil { + log.Error("Failed to check if rule exists. Error: ", err) + return err + } + + // No update required. Remove chain from chain map and return. + if exists { + delete(*chainMap, serviceChain) + return nil + } + } + } + + // Create new service chain name + log.Debug("Creating new service chain mapping for SNAT service: ", service) + serviceChain = servicePrefix + randSeq(maxChainLen-len(servicePrefix)) + serviceChains[service] = serviceChain + + // Create MEEP service chain + log.Debug("Creating MEEP chain ", serviceChain) + err = ipTbl.NewChain("nat", serviceChain) + if err != nil { + log.Error("Failed to create chain. Error: ", err) + return err + } + + // Create service routing rules + err = ipTbl.AppendUnique("nat", ingressSnatChain, "-j", serviceChain) + if err != nil { + log.Error("Failed to set rule [-A ", ingressSnatChain, " -j ", serviceChain, "]. Error: ", err) + return err + } + err = ipTbl.AppendUnique("nat", serviceChain, args...) + if err != nil { + log.Error("Failed to set rule [-A ", ingressSnatChain, " -j ", serviceChain, " ", args, "]. Error: ", err) + return err + } + flushRequired = true return nil } diff --git a/go-apps/meep-virt-engine/Dockerfile b/go-apps/meep-virt-engine/Dockerfile index e589449a26cd9696ede222f692a39db8b9addb18..39c5fac8561cbca1e404ef9d11fd91ae2d53fa73 100644 --- a/go-apps/meep-virt-engine/Dockerfile +++ b/go-apps/meep-virt-engine/Dockerfile @@ -26,6 +26,8 @@ RUN mkdir -p /active \ && chmod +x /usr/local/bin/helm \ && wget -q https://dl.k8s.io/release/v1.28.4/bin/linux/amd64/kubectl -O /usr/local/bin/kubectl \ && chmod +x /usr/local/bin/kubectl \ + && apt-get purge -y --auto-remove wget \ + && rm -rf /var/lib/apt/lists/* \ && chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/go-apps/meep-virt-engine/helm/delete.go b/go-apps/meep-virt-engine/helm/delete.go index 546ea8e5d8b0a40e361008cc459223055738a5e3..a1f02fe9ad87d6fa475889eef2bf280e5e4c8a04 100644 --- a/go-apps/meep-virt-engine/helm/delete.go +++ b/go-apps/meep-virt-engine/helm/delete.go @@ -18,14 +18,21 @@ package helm import ( "os/exec" + "sync" log "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger" ) func deleteReleases(charts []Chart) error { + var wg sync.WaitGroup for _, c := range charts { - deleteRelease(c) + wg.Add(1) + go func(chart Chart) { + defer wg.Done() + deleteRelease(chart) + }(c) } + wg.Wait() return nil } diff --git a/go-apps/meep-virt-engine/helm/helm.go b/go-apps/meep-virt-engine/helm/helm.go index f0c9a713d1d819191404f0a3a80ef72a5450d4b7..26cf3f5ddeb5954ed79669629da4d6eb5c03cf5a 100644 --- a/go-apps/meep-virt-engine/helm/helm.go +++ b/go-apps/meep-virt-engine/helm/helm.go @@ -36,3 +36,7 @@ func InstallCharts(charts []Chart, sandboxName string) error { func DeleteReleases(charts []Chart, sandboxName string) error { return runTask(Delete, charts, sandboxName) } + +func CleanChartDir(chartDir string) error { + return runCleanTask(chartDir) +} diff --git a/go-apps/meep-virt-engine/helm/install.go b/go-apps/meep-virt-engine/helm/install.go index c8864135ff864c4e49225f194ba1f7236265395e..1ddf90b4e401ed8db1708f713354f4bdca85e519 100644 --- a/go-apps/meep-virt-engine/helm/install.go +++ b/go-apps/meep-virt-engine/helm/install.go @@ -31,15 +31,26 @@ func installCharts(charts []Chart, sandboxName string) error { return err } + errChan := make(chan error, len(charts)) for _, chart := range charts { - err := install(chart) - if err != nil { - log.Info("Cleaning installed releases") - cleanReleases(charts, sandboxName) - return err + go func(c Chart) { + errChan <- install(c) + }(chart) + } + + var installErr error + for i := 0; i < len(charts); i++ { + if err := <-errChan; err != nil { + installErr = err } } + if installErr != nil { + log.Info("Cleaning installed releases") + cleanReleases(charts, sandboxName) + return installErr + } + return nil } diff --git a/go-apps/meep-virt-engine/helm/worker.go b/go-apps/meep-virt-engine/helm/worker.go index 19dc3f5c6c5db6e11b06a6e7887fc00c95a858b2..a06259023095499c3b676af6da9f492dec42b320 100644 --- a/go-apps/meep-virt-engine/helm/worker.go +++ b/go-apps/meep-virt-engine/helm/worker.go @@ -17,6 +17,8 @@ package helm import ( + "os" + log "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger" ) @@ -25,12 +27,14 @@ type Task string const ( Install Task = "INSTALL" Delete Task = "DELETE" + Clean Task = "CLEAN" ) type Job struct { task Task charts []Chart sandboxName string + chartDir string } var queue *chan Job = nil @@ -54,6 +58,13 @@ func startWorker() { log.Debug("Deleting ", len(job.charts), " Releases...") _ = deleteReleases(job.charts) log.Debug("Releases deleted (", len(job.charts), ")") + + case Clean: + log.Debug("Removing chart directory: ", job.chartDir) + if _, err := os.Stat(job.chartDir); err == nil { + _ = os.RemoveAll(job.chartDir) + } + log.Debug("Chart directory removed (", job.chartDir, ")") } } queue = nil @@ -66,3 +77,10 @@ func runTask(task Task, charts []Chart, sandboxName string) error { *queue <- job return nil } + +func runCleanTask(chartDir string) error { + startWorker() + var job Job = Job{task: Clean, chartDir: chartDir} + *queue <- job + return nil +} diff --git a/go-apps/meep-virt-engine/server/chart-template.go b/go-apps/meep-virt-engine/server/chart-template.go index 037a509ecffa7a6d40a04f4cdd020348239ebaa4..b78b0ebf1a44cc04fdfbba292799cdbc0521c553 100644 --- a/go-apps/meep-virt-engine/server/chart-template.go +++ b/go-apps/meep-virt-engine/server/chart-template.go @@ -129,7 +129,7 @@ type SandboxTemplate struct { HttpsOnly bool AuthEnabled bool IsMepService bool - LocationName string + LocationName string AppEnablement string Env []string } @@ -521,6 +521,9 @@ func deployCharts(charts []helm.Chart, sandboxName string) error { if len(tier) == 0 { // Cyclic dependency or missing dependency detected + for _, c := range remaining { + log.Error("Chart stuck: ", c.Name, " dependencies: ", c.Dependencies) + } return errors.New("Cyclic dependency or missing dependency detected in scenario") } @@ -769,6 +772,14 @@ func generateSandboxCharts(sandboxName string) (charts []helm.Chart, err error) charts = append(charts, chart) } + // Reorder charts so meep-sandbox-ctrl is deployed first + for i, chart := range charts { + if chart.Name == "meep-sandbox-ctrl" && i != 0 { + charts[0], charts[i] = charts[i], charts[0] + break + } + } + return charts, nil } diff --git a/go-apps/meep-virt-engine/server/virt-engine.go b/go-apps/meep-virt-engine/server/virt-engine.go index 19441cb7132796afb0f69d1c1aaf82a7f9c4936a..38e8c1b42df96b824f3ebdb360e18e8a02e91b67 100644 --- a/go-apps/meep-virt-engine/server/virt-engine.go +++ b/go-apps/meep-virt-engine/server/virt-engine.go @@ -211,9 +211,30 @@ func msgHandler(msg *mq.Msg, userData interface{}) { } } +func getModel(sandboxName string) *mod.Model { + activeModel := ve.activeModels[sandboxName] + if activeModel == nil { + modelCfg := mod.ModelCfg{ + Name: moduleName, + Namespace: sandboxName, + Module: moduleName, + DbAddr: redisAddr, + UpdateCb: nil, + } + var err error + activeModel, err = mod.NewModel(modelCfg) + if err != nil { + log.Error("Failed to create model: ", err.Error()) + return nil + } + ve.activeModels[sandboxName] = activeModel + } + return activeModel +} + func activateScenario(sandboxName string) { // Get sandbox-specific active model - activeModel := ve.activeModels[sandboxName] + activeModel := getModel(sandboxName) if activeModel == nil { log.Error("No active model for sandbox: ", sandboxName) return @@ -237,7 +258,7 @@ func addScenarioNode(sandboxName string, nodeName string) { log.Info("Adding node: ", nodeName) // Get sandbox-specific active model - activeModel := ve.activeModels[sandboxName] + activeModel := getModel(sandboxName) if activeModel == nil { log.Error("No active model for sandbox: ", sandboxName) return @@ -254,6 +275,20 @@ func addScenarioNode(sandboxName string, nodeName string) { log.Error("Error creating charts: ", err) return } + } else if mod.IsPhyLoc(nodeType) { + node := activeModel.GetNode(nodeName) + pl, ok := node.(*dataModel.PhysicalLocation) + if !ok { + log.Error("Error casting physical location: " + nodeName) + return + } + for _, proc := range pl.Processes { + err := Deploy(sandboxName, proc.Name, activeModel) + if err != nil { + log.Error("Error creating charts for process ", proc.Name, ": ", err) + continue + } + } } else { log.Error("Unsupported node type: ", nodeType) return @@ -264,7 +299,7 @@ func modifyScenarioNode(sandboxName string, nodeName string) { log.Info("Modifying node: ", nodeName) // Get sandbox-specific active model - activeModel := ve.activeModels[sandboxName] + activeModel := getModel(sandboxName) if activeModel == nil { log.Error("No active model for sandbox: ", sandboxName) return @@ -272,6 +307,10 @@ func modifyScenarioNode(sandboxName string, nodeName string) { // Get cached scenario name scenarioName := ve.activeScenarioNames[sandboxName] + if scenarioName == "" { + scenarioName = activeModel.GetScenarioName() + ve.activeScenarioNames[sandboxName] = scenarioName + } // Sync with active scenario store activeModel.UpdateScenario() @@ -304,7 +343,7 @@ func removeScenarioNode(sandboxName string, nodeName string) { } // Get sandbox-specific active model - activeModel := ve.activeModels[sandboxName] + activeModel := getModel(sandboxName) if activeModel == nil { log.Error("No active model for sandbox: ", sandboxName) return @@ -312,6 +351,10 @@ func removeScenarioNode(sandboxName string, nodeName string) { // Get cached scenario name scenarioName := ve.activeScenarioNames[sandboxName] + if scenarioName == "" { + scenarioName = activeModel.GetScenarioName() + ve.activeScenarioNames[sandboxName] = scenarioName + } // Before updating active scenario, find processes to remove procNames := []string{} @@ -358,6 +401,9 @@ func terminateScenario(sandboxName string, scenarioName string) { if scenarioName == "" { // Get cached scenario name scenarioName = ve.activeScenarioNames[sandboxName] + if scenarioName == "" && ve.activeModels[sandboxName] != nil { + scenarioName = ve.activeModels[sandboxName].GetScenarioName() + } } if scenarioName == "" { @@ -370,8 +416,8 @@ func terminateScenario(sandboxName string, scenarioName string) { log.Info("Number of charts to be deleted: ", chartsToDelete) ve.activeScenarioNames[sandboxName] = "" - // Clean up any leftover cluster role bindings - cleanUpClusterRoleBindings(sandboxName) + // Clean up any leftover cluster role bindings (do not delete sandbox pod bindings) + cleanUpClusterRoleBindings(sandboxName, false) // ticker := time.NewTicker(retryTimerDuration * time.Millisecond) @@ -394,8 +440,8 @@ func terminateScenario(sandboxName string, scenarioName string) { func createSandbox(sandboxName string) { var err error - // Clean up any leftover cluster role bindings first - cleanUpClusterRoleBindings(sandboxName) + // Clean up any leftover cluster role bindings first (clean all including old sandbox pod bindings) + cleanUpClusterRoleBindings(sandboxName, true) // Create new Model instance modelCfg := mod.ModelCfg{ @@ -426,8 +472,8 @@ func destroySandbox(sandboxName string) { ve.activeScenarioNames[sandboxName] = "" ve.activeModels[sandboxName] = nil - // Clean up any leftover cluster role bindings - cleanUpClusterRoleBindings(sandboxName) + // Clean up any leftover cluster role bindings (clean all when destroying sandbox) + cleanUpClusterRoleBindings(sandboxName, true) // ticker := time.NewTicker(retryTimerDuration * time.Millisecond) @@ -491,17 +537,17 @@ func deleteReleases(sandboxName string, scenarioName string, procName string) (e } } - // Then delete charts + // Then delete charts (queued sequentially in Helm worker) if _, err := os.Stat(path); err == nil { - log.Debug("Removing charts from path: ", path) - os.RemoveAll(path) + log.Debug("Queueing chart removal from path: ", path) + _ = helm.CleanChartDir(path) } } return err, chartsToDelete } -func cleanUpClusterRoleBindings(sandboxName string) { - log.Info("Cleaning up ClusterRoleBindings for sandbox: ", sandboxName) +func cleanUpClusterRoleBindings(sandboxName string, cleanAll bool) { + log.Info("Cleaning up ClusterRoleBindings for sandbox: ", sandboxName, " (cleanAll=", cleanAll, ")") cmd := exec.Command("kubectl", "get", "clusterrolebindings", "-o", "name") out, err := cmd.Output() if err != nil { @@ -509,11 +555,23 @@ func cleanUpClusterRoleBindings(sandboxName string) { return } + sboxPods := strings.Split(strings.TrimSpace(os.Getenv("MEEP_SANDBOX_PODS")), ",") + sboxPodMap := make(map[string]bool) + for _, pod := range sboxPods { + sboxPodMap[strings.TrimSpace(pod)] = true + } + lines := strings.Split(string(out), "\n") prefix := "clusterrolebinding.rbac.authorization.k8s.io/" + sandboxName + ":" for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, prefix) { + if !cleanAll { + podName := strings.TrimPrefix(line, prefix) + if sboxPodMap[podName] { + continue + } + } log.Info("Deleting leftover clusterrolebinding: ", line) deleteCmd := exec.Command("kubectl", "delete", line) _ = deleteCmd.Run() diff --git a/go-apps/meep-vis/entrypoint.sh b/go-apps/meep-vis/entrypoint.sh index a7c0ae96a1b22c27e3b8ef1dc0abe84822c4d1fe..b6f01c716d29b48e4d13875f439f6838a8aa19d4 100755 --- a/go-apps/meep-vis/entrypoint.sh +++ b/go-apps/meep-vis/entrypoint.sh @@ -7,7 +7,9 @@ echo "MEEP_LOCATION_NAME: ${MEEP_LOCATION_NAME}" echo "MEEP_CODECOV: ${MEEP_CODECOV}" echo "MEEP_POA_LIST: ${MEEP_POA_LIST}" # E.g. poa-5g1;poa-5g2 -MEEP_BROKER="wss://$MEEP_HOST_URL:1883/$MEEP_SANDBOX_NAME/$MEEP_LOCATION_NAME/meep-mosquitto" +TMP_URL="${MEEP_HOST_URL#https://}" +TMP_URL="${TMP_URL#http://}" +MEEP_BROKER="wss://${TMP_URL}:1883/$MEEP_SANDBOX_NAME/$MEEP_LOCATION_NAME/meep-mosquitto" if [[ ! -z "${MEEP_LOCATION_NAME}" ]]; then svcPath="${MEEP_SANDBOX_NAME}/${MEEP_LOCATION_NAME}" diff --git a/go-apps/meepctl/cmd/build.go b/go-apps/meepctl/cmd/build.go index ab471abbfbca1ce662c57fd8146708b0e29cc441..d2cc926cf6d5385253f0d1df7705e4ba03a370be 100644 --- a/go-apps/meepctl/cmd/build.go +++ b/go-apps/meepctl/cmd/build.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "sort" "strings" "time" @@ -33,6 +34,7 @@ import ( type BuildData struct { codecov bool nolint bool + noCache bool coreGoApps []string coreJsApps []string sandboxGoApps []string @@ -83,6 +85,7 @@ func init() { // Set build-specific flags buildCmd.Flags().BoolVar(&buildData.codecov, "codecov", false, "Build a code coverage binary (dev. option)") buildCmd.Flags().BoolVar(&buildData.nolint, "nolint", false, "Disable linting") + buildCmd.Flags().BoolVar(&buildData.noCache, "no-cache", false, "Build binaries without using checksum cache") // Add command rootCmd.AddCommand(buildCmd) @@ -184,34 +187,10 @@ func buildFrontend(targetName string, repo string, cobraCmd *cobra.Command) { locDeps := utils.RepoCfg.GetStringMapString(repo + targetName + ".local-deps") // Checksum optimization - binDirExists := false - if _, err := os.Stat(binDir); !os.IsNotExist(err) { - binDirExists = true - } - - checksumFile := srcDir + "/.build_checksum" - findArgs := srcDir - for _, depDir := range locDeps { - findArgs += " " + gitDir + "/" + depDir - } - - cmdStr := fmt.Sprintf("find %s -type f -not -name '.build_checksum' -not -path '*/node_modules/*' -not -path '*/dist/*' -not -path '*/bin/*' -not -path '*/.git/*' 2>/dev/null | sort | xargs md5sum 2>/dev/null | md5sum | cut -d' ' -f1", findArgs) - checksumCmd := exec.Command("sh", "-c", cmdStr) - outBytes, err := checksumCmd.Output() - currentChecksum := "" - if err == nil { - currentChecksum = strings.TrimSpace(string(outBytes)) - - if binDirExists && currentChecksum != "" { - savedChecksumBytes, err := os.ReadFile(checksumFile) - if err == nil { - savedChecksum := strings.TrimSpace(string(savedChecksumBytes)) - if currentChecksum == savedChecksum { - fmt.Println(utils.FormatStep(" + skipping build (no changes detected)")) - return - } - } - } + checksumFile, currentChecksum, skipped := checkBuildCache(srcDir, binDir, binDir, gitDir, locDeps) + if skipped { + fmt.Println(utils.FormatStep(" + skipping build (no changes detected)")) + return } // dependencies @@ -222,6 +201,7 @@ func buildFrontend(targetName string, repo string, cobraCmd *cobra.Command) { if err != nil { fmt.Println(utils.FormatError("Error: " + err.Error())) fmt.Println(out) + os.Exit(1) } if len(locDeps) > 0 { @@ -266,7 +246,7 @@ func buildFrontend(targetName string, repo string, cobraCmd *cobra.Command) { //build fmt.Println(utils.FormatStep(" + building " + targetName)) - + envFlags := utils.RepoCfg.GetStringMapString(repo + targetName + ".env") cmd = exec.Command("npm", "run", "build", "--", "--output-path="+binDir, "--env.VERSION=v"+version) @@ -275,18 +255,13 @@ func buildFrontend(targetName string, repo string, cobraCmd *cobra.Command) { for k, v := range envFlags { cmd.Env = append(cmd.Env, k+"="+v) } - + out, err = utils.ExecuteCmd(cmd, cobraCmd) if err != nil { fmt.Println(utils.FormatError("Error: " + err.Error())) fmt.Println(out) } else { - if currentChecksum != "" { - err = os.WriteFile(checksumFile, []byte(currentChecksum), 0644) - if err != nil { - fmt.Println(utils.FormatError("Error saving checksum: " + err.Error())) - } - } + saveBuildCache(checksumFile, currentChecksum) } } @@ -299,6 +274,13 @@ func buildGoApp(targetName string, repo string, cobraCmd *cobra.Command) { codecovCapable := utils.RepoCfg.GetBool(repo + targetName + ".codecov") lintEnabled := utils.RepoCfg.GetBool(repo + targetName + ".lint") + locDeps := utils.RepoCfg.GetStringMapString(repo + targetName + ".local-deps") + checksumFile, currentChecksum, skipped := checkBuildCache(srcDir, binDir+"/"+targetName, binDir, gitDir, locDeps) + if skipped { + fmt.Println(utils.FormatStep(" + skipping build (no changes detected)")) + return + } + // dependencies fmt.Println(utils.FormatStep(" + checking external dependencies")) cmd := exec.Command("go", "mod", "vendor") @@ -349,6 +331,8 @@ func buildGoApp(targetName string, repo string, cobraCmd *cobra.Command) { if err != nil { fmt.Println(utils.FormatError("Error: " + err.Error())) fmt.Println(out) + } else { + saveBuildCache(checksumFile, currentChecksum) } } @@ -381,3 +365,48 @@ func fixDeps(targetName string, repo string, cobraCmd *cobra.Command) { } } } + +func checkBuildCache(srcDir string, binFile string, binDir string, gitDir string, locDeps map[string]string) (string, string, bool) { + binExists := false + if _, err := os.Stat(binFile); !os.IsNotExist(err) { + binExists = true + } + + checksumFile := binDir + "/.build_checksum" + findArgs := srcDir + for _, depDir := range locDeps { + findArgs += " " + gitDir + "/" + depDir + } + + cmdStr := fmt.Sprintf("find %s -type f -not -name '.build_checksum' -not -path '*/node_modules/*' -not -path '*/vendor/*' -not -path '*/dist/*' -not -path '*/bin/*' -not -path '*/.git/*' 2>/dev/null | sort | xargs md5sum 2>/dev/null | md5sum | cut -d' ' -f1", findArgs) + checksumCmd := exec.Command("sh", "-c", cmdStr) + outBytes, err := checksumCmd.Output() + currentChecksum := "" + if err == nil { + currentChecksum = strings.TrimSpace(string(outBytes)) + if !buildData.noCache && binExists && currentChecksum != "" { + savedChecksumBytes, err := os.ReadFile(checksumFile) + if err == nil { + savedChecksum := strings.TrimSpace(string(savedChecksumBytes)) + if currentChecksum == savedChecksum { + return checksumFile, currentChecksum, true + } + } + } + } + return checksumFile, currentChecksum, false +} + +func saveBuildCache(checksumFile string, currentChecksum string) { + if currentChecksum != "" { + err := os.MkdirAll(filepath.Dir(checksumFile), 0755) + if err != nil { + fmt.Println(utils.FormatError("Error creating directory: " + err.Error())) + return + } + err = os.WriteFile(checksumFile, []byte(currentChecksum), 0644) + if err != nil { + fmt.Println(utils.FormatError("Error saving checksum: " + err.Error())) + } + } +} diff --git a/go-apps/meepctl/cmd/delete.go b/go-apps/meepctl/cmd/delete.go index 436483c3dba78b49bf94877727b8c8a2b88f8b00..8309ae89fb5db5ee8219aac7f796e920c003d5d0 100644 --- a/go-apps/meepctl/cmd/delete.go +++ b/go-apps/meepctl/cmd/delete.go @@ -18,10 +18,11 @@ package cmd import ( "fmt" + + "sync" "time" "github.com/InterDigitalInc/AdvantEDGE/go-apps/meepctl/utils" - "github.com/spf13/cobra" ) @@ -101,7 +102,8 @@ func deleteRun(cmd *cobra.Command, args []string) { switch group { case "core": if targetApp != "" { - k8sDelete(targetApp, cmd) + out := k8sDelete(targetApp, cmd) + fmt.Print(out) } else { deleteApps(deleteData.coreApps, cmd) } @@ -120,9 +122,23 @@ func deleteRun(cmd *cobra.Command, args []string) { } func deleteApps(apps []string, cobraCmd *cobra.Command) { + var wg sync.WaitGroup + var printMutex sync.Mutex + for _, app := range apps { - k8sDelete(app, cobraCmd) + wg.Add(1) + go func(appName string) { + defer wg.Done() + output := k8sDelete(appName, cobraCmd) + + if output != "" { + printMutex.Lock() + fmt.Print(output) + printMutex.Unlock() + } + }(app) } + wg.Wait() } // Delete a single dep app @@ -143,17 +159,30 @@ func deleteSingleDepApp(app string, cobraCmd *cobra.Command) { } return } - k8sDelete(app, cobraCmd) + out := k8sDelete(app, cobraCmd) + fmt.Print(out) } -func k8sDelete(component string, cobraCmd *cobra.Command) { +func k8sDelete(component string, cobraCmd *cobra.Command) string { + var out string // If release exist - exist, _ := utils.IsHelmRelease(component, cobraCmd) + exist, outRel, _ := utils.IsHelmRelease(component, cobraCmd) + out += outRel if exist { + switch component { + case "meep-prometheus": + // PVCs are created by StatefulSet and shouldn't be deleted manually if we want to retain data + case "meep-thanos": + // PVCs are kept by helm resource-policy, so we don't manually delete them + case "meep-thanos-archive": + // PVCs are kept by helm resource-policy, so we don't manually delete them + } // Delete - err := utils.HelmDelete(component, cobraCmd) + outDel, err := utils.HelmDelete(component, cobraCmd) + out += outDel if err != nil { - fmt.Println("Helm delete failed with Error: ", err) + out += fmt.Sprintf("Helm delete failed with Error: %v\n", err) } } + return out } diff --git a/go-apps/meepctl/cmd/deploy.go b/go-apps/meepctl/cmd/deploy.go index 59d0e881ab4440e3d1fa01dc63583052387f32d7..4386a35d9a860e6b756c8206c13fcc538a7de1a5 100644 --- a/go-apps/meepctl/cmd/deploy.go +++ b/go-apps/meepctl/cmd/deploy.go @@ -19,11 +19,14 @@ package cmd import ( "errors" "fmt" + "net" "os" "os/exec" "sort" + "strconv" "strings" + "sync" "time" "github.com/InterDigitalInc/AdvantEDGE/go-apps/meepctl/utils" @@ -217,7 +220,10 @@ func deployDepRun(cmd *cobra.Command, args []string) { createCRD(cmd) deployDep(cmd) } else { - deploySingleDepApp(target, cmd) + out := deploySingleDepApp(target, cmd) + if out != "" { + fmt.Print(out) + } } } @@ -253,7 +259,8 @@ func deployCoreRun(cmd *cobra.Command, args []string) { if target == "all" { deployCore(cmd) } else { - deploySingleApp(target, cmd) + out := deploySingleApp(target, cmd) + fmt.Print(out) } } @@ -297,10 +304,17 @@ func deployEnsureStorage(cobraCmd *cobra.Command) { // running meepctl (or if run with sudo), the directories are permissive enough // for the Kubernetes kubelet to successfully apply the chart's securityContext.fsGroup // without encountering "permission denied" errors. + uidStr := utils.RepoCfg.GetString("repo.deployment.permissions.uid") + gidStr := utils.RepoCfg.GetString("repo.deployment.permissions.gid") + uid, _ := strconv.Atoi(uidStr) + gid, _ := strconv.Atoi(gidStr) + for _, dir := range dirs { err := os.MkdirAll(dir, 0777) if err != nil { fmt.Println("Error creating path ["+dir+"]:", err) + } else { + _ = os.Chown(dir, uid, gid) } } } @@ -310,13 +324,40 @@ func deployCore(cobraCmd *cobra.Command) { // Code coverage storage deployCodeCovStorage(cobraCmd) + var wg sync.WaitGroup + var printMutex sync.Mutex + + total := len(deployData.coreApps) + var current int + for _, app := range deployData.coreApps { - deploySingleApp(app, cobraCmd) + wg.Add(1) + go func(appName string) { + defer wg.Done() + output := deploySingleApp(appName, cobraCmd) + if output != "" { + printMutex.Lock() + current++ + fmt.Printf("%s Processed %s\n", getProgressBar(current, total), appName) + fmt.Print(output) + printMutex.Unlock() + } + }(app) } + wg.Wait() } // Deploy a single core app -func deploySingleApp(app string, cobraCmd *cobra.Command) { +func deploySingleApp(app string, cobraCmd *cobra.Command) string { + var out string + force, _ := cobraCmd.Flags().GetBool("force") + exist, outRel, _ := utils.IsHelmRelease(app, cobraCmd) + out += outRel + if exist && !force { + out += fmt.Sprintf("%v\n", utils.FormatWarning("Skipping "+app+": already deployed -- use [-f, --force] flag to force deployment")) + return out + } + chart := deployData.gitdir + "/" + utils.RepoCfg.GetString("repo.core.go-apps."+app+".chart") codecov := utils.RepoCfg.GetBool("repo.core.go-apps." + app + ".codecov") onboardedapp := utils.RepoCfg.GetBool("repo.core.go-apps." + app + ".onboardedapp") @@ -351,11 +392,14 @@ func deploySingleApp(app string, cobraCmd *cobra.Command) { } if httpsOnly { coreFlags = utils.HelmFlags(coreFlags, "--set", "image.env.MEEP_HOST_URL=https://"+hostName) + coreFlags = utils.HelmFlags(coreFlags, "--set", "image.env.MEEP_HTTPS_ONLY=true") } else { coreFlags = utils.HelmFlags(coreFlags, "--set", "image.env.MEEP_HOST_URL=http://"+hostName) + coreFlags = utils.HelmFlags(coreFlags, "--set", "image.env.MEEP_HTTPS_ONLY=false") } - k8sDeploy(app, chart, coreFlags, cobraCmd) + out += k8sDeploy(app, chart, coreFlags, cobraCmd) + return out } // Create CRDs @@ -372,15 +416,19 @@ func createCRD(cobraCmd *cobra.Command) { // Deploy dependencies func deployDep(cobraCmd *cobra.Command) { - for _, app := range deployData.depApps { - chart := deployData.gitdir + "/" + utils.RepoCfg.GetString("repo.dep."+app+".chart") - flags := deployRunScriptsAndGetFlags(app, chart, cobraCmd) - k8sDeploy(app, chart, flags, cobraCmd) + total := len(deployData.depApps) + for i, app := range deployData.depApps { + fmt.Printf("%s Deploying %s...\n", getProgressBar(i+1, total), app) + output := deploySingleDepApp(app, cobraCmd) + if output != "" { + fmt.Print(output) + } } } // Deploy a single dep app -func deploySingleDepApp(app string, cobraCmd *cobra.Command) { +func deploySingleDepApp(app string, cobraCmd *cobra.Command) string { + var out string // Validate the app is in the dep list found := false for _, depApp := range deployData.depApps { @@ -390,22 +438,59 @@ func deploySingleDepApp(app string, cobraCmd *cobra.Command) { } } if !found { - fmt.Println(utils.FormatError("Error: '" + app + "' is not a valid dep target")) - fmt.Println(utils.FormatStep("Valid dep targets:")) + out += fmt.Sprintf("%s\n", utils.FormatError("Error: '"+app+"' is not a valid dep target")) + out += fmt.Sprintf("%s\n", utils.FormatStep("Valid dep targets:")) for _, a := range deployData.depApps { - fmt.Println(utils.FormatStep(" * " + a)) + out += fmt.Sprintf("%s\n", utils.FormatStep(" * "+a)) } - return + return out } + + force, _ := cobraCmd.Flags().GetBool("force") + exist, outRel, _ := utils.IsHelmRelease(app, cobraCmd) + out += outRel + if exist && !force { + out += fmt.Sprintf("%s\n", utils.FormatWarning("Skipping "+app+": already deployed -- use [-f, --force] flag to force deployment")) + return out + } + + if app == "meep-tilt" { + err := dockerizeMeepTilt(cobraCmd) + if err != nil { + out += fmt.Sprintf("%s\n", utils.FormatError(err.Error())) + return out + } + } + chart := deployData.gitdir + "/" + utils.RepoCfg.GetString("repo.dep."+app+".chart") flags := deployRunScriptsAndGetFlags(app, chart, cobraCmd) - k8sDeploy(app, chart, flags, cobraCmd) + out += k8sDeploy(app, chart, flags, cobraCmd) + return out +} + +// Exceptional case for meep-tilt +func dockerizeMeepTilt(cobraCmd *cobra.Command) error { + // fmt.Println(utils.FormatStep("Deploying meep-tilt")) + imageName := deployData.registry + "/meep-tilt:" + deployData.tag + tiltDir := deployData.gitdir + "/go-apps/meep-tilt" + + buildCmd := exec.Command("docker", "build", "-t", imageName, tiltDir) + _, err := utils.ExecuteCmd(buildCmd, cobraCmd) + if err != nil { + return errors.New("Error building meep-tilt: " + err.Error()) + } + + pushCmd := exec.Command("docker", "push", imageName) + _, err = utils.ExecuteCmd(pushCmd, cobraCmd) + if err != nil { + return errors.New("Error pushing meep-tilt: " + err.Error()) + } + return nil } func deployRunScriptsAndGetFlags(targetName string, chart string, cobraCmd *cobra.Command) [][]string { var flags [][]string authUrlAnnotation := "ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/auth-url" - authUrl := "https://$http_host/auth/v1/authenticate" userValueDir := deployData.workdir + "/user/values" @@ -422,9 +507,12 @@ func deployRunScriptsAndGetFlags(targetName string, chart string, cobraCmd *cobr // Common platform flags httpsOnly := utils.RepoCfg.GetBool("repo.deployment.ingress.https-only") + scheme := "http" if httpsOnly { - flags = utils.HelmFlags(flags, "--set", "ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/force-ssl-redirect=true") + scheme = "https" + flags = utils.HelmFlags(flags, "--set-string", "ingress.annotations.nginx\\.ingress\\.kubernetes\\.io/force-ssl-redirect=true") } + authUrl := scheme + "://$http_host/auth/v1/authenticate" // Service-specific flags switch targetName { @@ -771,7 +859,6 @@ func deployRunScriptsAndGetFlags(targetName string, chart string, cobraCmd *cobr flags = utils.HelmFlags(flags, "--set", "persistence.location="+deployData.workdir+"/virt-engine") flags = utils.HelmFlags(flags, "--set", "user.values.location="+deployData.workdir+"/user/values") flags = utils.HelmFlags(flags, "--set", "image.env.MEEP_SANDBOX_PODS="+getItemList(virtEngineTarget+".sandbox-pods")) - flags = utils.HelmFlags(flags, "--set", "image.env.MEEP_HTTPS_ONLY="+strconv.FormatBool(httpsOnly)) flags = utils.HelmFlags(flags, "--set", "image.env.MEEP_USER_SWAGGER="+strconv.FormatBool(userSwagger)) case "meep-webhook": cert, key, cabundle := deployCreateWebhookCerts(chart, cobraCmd) @@ -786,25 +873,27 @@ func deployRunScriptsAndGetFlags(targetName string, chart string, cobraCmd *cobr return flags } -func k8sDeploy(app string, chart string, flags [][]string, cobraCmd *cobra.Command) { +func k8sDeploy(app string, chart string, flags [][]string, cobraCmd *cobra.Command) string { + var out string force, _ := cobraCmd.Flags().GetBool("force") // If release exist && --force, delete - exist, _ := utils.IsHelmRelease(app, cobraCmd) + exist, outRel, _ := utils.IsHelmRelease(app, cobraCmd) + out += outRel if exist { if force { - _ = utils.HelmDelete(app, cobraCmd) - // Wait for K8s to clean up resources before redeploying - fmt.Println(utils.FormatWarning("Waiting 5s for resource cleanup...")) - time.Sleep(10 * time.Second) + outDel, _ := utils.HelmDelete(app, cobraCmd) + out += outDel } else { - fmt.Println(utils.FormatWarning("Skipping " + app + ": already deployed -- use [-f, --force] flag to force deployment")) - return + out += fmt.Sprintf("%v\n", utils.FormatWarning("Skipping "+app+": already deployed -- use [-f, --force] flag to force deployment")) + return out } } // Deploy - _ = utils.HelmInstall(app, chart, flags, cobraCmd) + outIns, _ := utils.HelmInstall(app, chart, flags, cobraCmd) + out += outIns + return out } func deployCodeCovStorage(cobraCmd *cobra.Command) { @@ -847,6 +936,54 @@ func deployCreateRegistryCerts(chart string, cobraCmd *cobra.Command) { certdir := deployData.workdir + "/certs" cmd := exec.Command("sh", "-c", chart+"/create-k8s-ca-signed-cert.sh --certdir "+certdir) _, _ = utils.ExecuteCmd(cmd, cobraCmd) + ca := utils.RepoCfg.GetString("repo.deployment.ingress.ca") + host := utils.RepoCfg.GetString("repo.deployment.ingress.host") + isIP := false + if ip := net.ParseIP(host); ip != nil { + isIP = true + } + if ca == "self-signed" || isIP { + trustRegistryCerts(certdir, cobraCmd) + } +} + +func trustRegistryCerts(certdir string, cobraCmd *cobra.Command) { + registry := deployData.registry + + // Copy Kubernetes CA and registry certs to system CA trust store + cmd := exec.Command("sudo", "cp", "-f", "/etc/kubernetes/pki/ca.crt", "/usr/local/share/ca-certificates/kubernetes-ca.crt") + _, _ = utils.ExecuteCmd(cmd, cobraCmd) + cmd = exec.Command("sh", "-c", "for f in "+certdir+"/*.pem; do [ -f \"$f\" ] && case \"$f\" in *-key.pem) continue;; esac && sudo cp -f \"$f\" \"/usr/local/share/ca-certificates/$(basename \"$f\" .pem).crt\"; done") + _, _ = utils.ExecuteCmd(cmd, cobraCmd) + cmd = exec.Command("sudo", "update-ca-certificates") + _, _ = utils.ExecuteCmd(cmd, cobraCmd) + + // Configure containerd v2.x registry host trust. + // Containerd v2 no longer uses the system CA store for image pulls; + // it requires explicit per-registry hosts.toml under /etc/containerd/certs.d/. + certsDir := "/etc/containerd/certs.d/" + registry + hostsToml := fmt.Sprintf(`server = "https://%s" + +[host."https://%s"] + ca = "/usr/local/share/ca-certificates/kubernetes-ca.crt" +`, registry, registry) + + cmd = exec.Command("sudo", "mkdir", "-p", certsDir) + _, _ = utils.ExecuteCmd(cmd, cobraCmd) + cmd = exec.Command("sudo", "sh", "-c", "cat > "+certsDir+"/hosts.toml <<'HOSTSEOF'\n"+hostsToml+"HOSTSEOF") + _, _ = utils.ExecuteCmd(cmd, cobraCmd) + + // Set containerd registry config_path to /etc/containerd/certs.d + // Uses targeted sed to only update the registry section's config_path + cmd = exec.Command("sudo", "sed", "-i", + `/\[plugins.*images.*registry\]/,/config_path/{s|config_path = .*|config_path = "/etc/containerd/certs.d"|}`, + "/etc/containerd/config.toml") + _, _ = utils.ExecuteCmd(cmd, cobraCmd) + + cmd = exec.Command("sudo", "systemctl", "restart", "containerd") + _, _ = utils.ExecuteCmd(cmd, cobraCmd) + cmd = exec.Command("sudo", "systemctl", "restart", "docker") + _, _ = utils.ExecuteCmd(cmd, cobraCmd) } func deployCreateIngressCerts(chart string, cobraCmd *cobra.Command) { @@ -873,3 +1010,11 @@ func getItemList(target string) string { } return itemListStr } + +func getProgressBar(current, total int) string { + width := 20 + percent := float64(current) / float64(total) + completed := int(percent * float64(width)) + uncompleted := width - completed + return fmt.Sprintf("[%s%s] %d/%d", strings.Repeat("=", completed), strings.Repeat("-", uncompleted), current, total) +} diff --git a/go-apps/meepctl/cmd/dockerize.go b/go-apps/meepctl/cmd/dockerize.go index b1353fc5af871b55b845971737e897ca8f984f18..acf0ffe94fa479250a85d35175d29b25e24432d0 100644 --- a/go-apps/meepctl/cmd/dockerize.go +++ b/go-apps/meepctl/cmd/dockerize.go @@ -179,7 +179,7 @@ func dockerize(targetName string, repo string, cobraCmd *cobra.Command) { err := os.MkdirAll(bindir, 0755) if err != nil { fmt.Println(utils.FormatError("Error: Failed to create bin directory: " + err.Error())) - return + os.Exit(1) } } // Copy Dockerfile @@ -190,7 +190,7 @@ func dockerize(targetName string, repo string, cobraCmd *cobra.Command) { if err != nil { fmt.Println(utils.FormatError("Error: " + err.Error())) fmt.Println(out) - return + os.Exit(1) } // copy service api files locally @@ -232,13 +232,12 @@ func dockerize(targetName string, repo string, cobraCmd *cobra.Command) { _, err = utils.ExecuteCmd(cmd, cobraCmd) if err != nil { fmt.Println(utils.FormatError("Error: Failed to dockerize " + tag + " with error: " + err.Error())) - return } cmd = exec.Command("docker", "push", tag) _, err = utils.ExecuteCmd(cmd, cobraCmd) if err != nil { fmt.Println(utils.FormatError("Error: Failed to push " + tag + " with error: " + err.Error())) - return + os.Exit(1) } } else { buildArgs = append(buildArgs, targetName, bindir) diff --git a/go-apps/meepctl/cmd/root.go b/go-apps/meepctl/cmd/root.go index a439c5483dd3792bbed2e4b33c3804b11cfe85a6..928f15a7226cd8efddc1d3cf7ae0760648cd36b2 100644 --- a/go-apps/meepctl/cmd/root.go +++ b/go-apps/meepctl/cmd/root.go @@ -91,4 +91,17 @@ func initConfig() { if err := viper.ReadInConfig(); err == nil { fmt.Println("Using meepctl config file:", viper.ConfigFileUsed()) } + + // Ensure Go and GOPATH/bin are in PATH so meepctl can find go and golangci-lint + path := os.Getenv("PATH") + home, _ := homedir.Dir() + if home != "" { + gocodeBin := home + "/gocode/bin" + if path == "" { + path = "/usr/local/go/bin:" + gocodeBin + } else { + path = path + ":/usr/local/go/bin:" + gocodeBin + } + _ = os.Setenv("PATH", path) + } } diff --git a/go-apps/meepctl/install.sh b/go-apps/meepctl/install.sh index 52f37dd331178cf7cc0e9979129ab549ff93be0b..6f2d394dfbf2dfa190fd88e586085435f691e34a 100755 --- a/go-apps/meepctl/install.sh +++ b/go-apps/meepctl/install.sh @@ -1,37 +1,76 @@ #!/bin/bash +set -e # Exit immediately on errors + +# Colors +CYAN='\033[0;36m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' # No Color + # Get full path to script directory SCRIPT=$(readlink -f "$0") BASEDIR=$(dirname "$SCRIPT") # Configure environment -GOOS=linux -IMAGE_NAME=meepctl -BINDIR=../../bin/meepctl -echo "$IMAGE_NAME" +export PATH=$PATH:/usr/local/go/bin:$HOME/gocode/bin +export GOOS=linux +IMAGE_NAME="meepctl" +BINDIR="../../bin/meepctl" + +echo "" +printf "%b\n" "${CYAN}${BOLD}==================================================${NC}" +printf "%b\n" "${CYAN}${BOLD}🚀 Installing Go Application: ${IMAGE_NAME}${NC}" +printf "%b\n" "${CYAN}${BOLD}==================================================${NC}" +echo "" -cd $BASEDIR +cd "$BASEDIR" || exit 1 # Clean build -echo "...clean" +printf "%b\n" "${BLUE}${BOLD}➤ Cleaning build artifacts${NC}" go clean +echo "" # Create vendor folder -echo "...vendor" +printf "%b\n" "${BLUE}${BOLD}➤ Vendoring dependencies${NC}" go mod vendor +echo "" # Lint code -echo "...lint" +printf "%b\n" "${BLUE}${BOLD}➤ Linting codebase${NC}" golangci-lint run +echo "" # Build -echo "...build" -go build -o ./$IMAGE_NAME . +printf "%b\n" "${BLUE}${BOLD}➤ Compiling binary${NC}" +go build -o "./$IMAGE_NAME" . +echo "" # Copy to bin folder -mkdir -p $BINDIR -cp ./$IMAGE_NAME $BINDIR +printf "%b\n" "${BLUE}${BOLD}➤ Copying binary to ${BINDIR}${NC}" +mkdir -p "$BINDIR" +cp "./$IMAGE_NAME" "$BINDIR" +echo "" # Install -echo "...install" +printf "%b\n" "${BLUE}${BOLD}➤ Running go install${NC}" go install + +echo "" +# Configure sudoers for meepctl certificate trust operations (when HTTPS) +printf "%b\n" "${BLUE}${BOLD}➤ Configuring sudoers NOPASSWD for meepctl certificate operations${NC}" +TARGET_USER="${SUDO_USER:-$USER}" +SUDOERS_RULE="${TARGET_USER} ALL=(ALL:ALL) NOPASSWD: /usr/bin/cp, /bin/cp, /usr/sbin/update-ca-certificates, /usr/bin/update-ca-certificates, /usr/bin/systemctl restart containerd, /usr/bin/systemctl restart docker, /bin/systemctl restart containerd, /bin/systemctl restart docker, /usr/bin/mkdir, /bin/mkdir, /usr/bin/sh, /bin/sh, /usr/bin/sed, /bin/sed" +if [ -n "$SUDO_PASSWORD" ]; then + echo "$SUDO_PASSWORD" | sudo -S sh -c "echo '$SUDOERS_RULE' > /etc/sudoers.d/meepctl && chmod 0440 /etc/sudoers.d/meepctl" 2>/dev/null || echo "⚠️ WARNING: Failed to create /etc/sudoers.d/meepctl (sudo required)" +elif sudo -n true 2>/dev/null; then + sudo sh -c "echo '$SUDOERS_RULE' > /etc/sudoers.d/meepctl && chmod 0440 /etc/sudoers.d/meepctl" 2>/dev/null || echo "⚠️ WARNING: Failed to create /etc/sudoers.d/meepctl (sudo required)" +else + sudo sh -c "echo '$SUDOERS_RULE' > /etc/sudoers.d/meepctl && chmod 0440 /etc/sudoers.d/meepctl" || echo "⚠️ WARNING: Failed to create /etc/sudoers.d/meepctl (sudo required)" +fi + +echo "" +printf "%b\n" "${GREEN}${BOLD}✨ ${IMAGE_NAME} installation completed successfully!${NC}" +echo "" diff --git a/go-apps/meepctl/utils/helm.go b/go-apps/meepctl/utils/helm.go index 136b896a1dcd1d44236b096804ef23f39dce566e..bdbef2711f745f97588166805a8f3111d4b21693 100644 --- a/go-apps/meepctl/utils/helm.go +++ b/go-apps/meepctl/utils/helm.go @@ -27,20 +27,20 @@ import ( ) // IsHelmRelease Returns true if a Helm release exists -func IsHelmRelease(name string, cobraCmd *cobra.Command) (exist bool, err error) { +func IsHelmRelease(name string, cobraCmd *cobra.Command) (exist bool, output string, err error) { exist = false verbose, _ := cobraCmd.Flags().GetBool("verbose") start := time.Now() cmd := exec.Command("helm", "ls", "--filter", name, "--short") if verbose { - fmt.Println("Cmd:", cmd.Args) + output += fmt.Sprintf("Cmd: %v\n", cmd.Args) } out, err := cmd.CombinedOutput() elapsed := time.Since(start) if err != nil { err = errors.New("Error listing component [" + name + "]") - fmt.Println(err) + output += fmt.Sprintf("%v\n", err) } else { s := string(out) lines := strings.Split(s, "\n") @@ -53,40 +53,38 @@ func IsHelmRelease(name string, cobraCmd *cobra.Command) (exist bool, err error) } if verbose { r := FormatResult("Result: "+string(out), elapsed, cobraCmd) - fmt.Println(r) + output += fmt.Sprintf("%v\n", r) } - return exist, err + return exist, output, err } -// HelmDelete Deletes specified release -func HelmDelete(name string, cobraCmd *cobra.Command) (err error) { +func HelmDelete(name string, cobraCmd *cobra.Command) (output string, err error) { verbose, _ := cobraCmd.Flags().GetBool("verbose") start := time.Now() - cmd := exec.Command("helm", "uninstall", name) + cmd := exec.Command("helm", "uninstall", name, "--wait") if verbose { - fmt.Println("Cmd:", cmd.Args) + output += fmt.Sprintf("Cmd: %v\n", cmd.Args) } out, err := cmd.CombinedOutput() elapsed := time.Since(start) if err != nil { err = errors.New("Error deleting component [" + name + "]") - fmt.Println(err) + output += fmt.Sprintf("%v\n", FormatError(err.Error())) } else { r := FormatResult("Deleted "+name, elapsed, cobraCmd) - fmt.Println(r) + output += fmt.Sprintf("%v\n", FormatWarning(r)) } if verbose { - fmt.Println("Result: " + string(out)) + output += fmt.Sprintf("Result: %v\n", string(out)) } - return err + return output, err } -// HelmInstall Install specified releases -func HelmInstall(name string, chart string, flags [][]string, cobraCmd *cobra.Command) (err error) { +func HelmInstall(name string, chart string, flags [][]string, cobraCmd *cobra.Command) (output string, err error) { verbose, _ := cobraCmd.Flags().GetBool("verbose") start := time.Now() @@ -96,21 +94,21 @@ func HelmInstall(name string, chart string, flags [][]string, cobraCmd *cobra.Co cmd.Args = append(cmd.Args, f[1]) } if verbose { - fmt.Println("Cmd:", cmd.Args) + output += fmt.Sprintf("Cmd: %v\n", cmd.Args) } out, err := cmd.CombinedOutput() elapsed := time.Since(start) if err != nil { err = errors.New("Error installing component [" + name + "]") - fmt.Println(err) + output += fmt.Sprintf("%v\n", FormatError(err.Error())) } else { r := FormatResult("Deployed "+name, elapsed, cobraCmd) - fmt.Println(r) + output += fmt.Sprintf("%v\n", FormatSuccess(r)) } if verbose { - fmt.Println("Result: " + string(out)) + output += fmt.Sprintf("Result: %v\n", string(out)) } - return err + return output, err } // HelmFlags Takes helm flag & value pair and formats it into an array of flag value pair diff --git a/go-packages/meep-model/model.go b/go-packages/meep-model/model.go index b22a9cd8a98e66a664c46309973befbbb17435a7..d1b4ab26d85f0fe232378ee20d4d292099e4301f 100644 --- a/go-packages/meep-model/model.go +++ b/go-packages/meep-model/model.go @@ -540,6 +540,7 @@ func (m *Model) addPhyLoc(node *dataModel.ScenarioNode, parentNode *Node) (err e // Get parent Network Location node & context information nl := parentNode.object.(*dataModel.NetworkLocation) + nlCtx := parentNode.context.(*NodeContext) // Validate Physical Location if node.NodeDataUnion == nil || node.NodeDataUnion.PhysicalLocation == nil { @@ -551,12 +552,30 @@ func (m *Model) addPhyLoc(node *dataModel.ScenarioNode, parentNode *Node) (err e return err } - // Ignore any configured processes - pl.Processes = make([]dataModel.Process, 0) - // Add PhyLoc to parent NetLoc nl.PhysicalLocations = append(nl.PhysicalLocations, *pl) + // Add configured processes + for iProc := range pl.Processes { + proc := &pl.Processes[iProc] + + // Create node context (Scenario -> Domain -> Zone -> NetLoc -> PhyLoc) + procCtx := NewNodeContext(nlCtx.Parents[Deployment], nlCtx.Parents[Domain], nlCtx.Parents[Zone], nl.Name, pl.Name) + + // Add to node map and network graph + m.nodeMap.AddNode(NewNode(proc.Id, proc.Name, proc.Type_, proc, nil, pl, procCtx)) + m.networkGraph.AddNode(proc.Name, pl.Name, false) + + // Update service map for external processes + if proc.IsExternal { + var nodeServiceMaps dataModel.NodeServiceMaps + nodeServiceMaps.Node = proc.Name + nodeServiceMaps.IngressServiceMap = append(nodeServiceMaps.IngressServiceMap, proc.ExternalConfig.IngressServiceMap...) + nodeServiceMaps.EgressServiceMap = append(nodeServiceMaps.EgressServiceMap, proc.ExternalConfig.EgressServiceMap...) + m.svcMap = append(m.svcMap, nodeServiceMaps) + } + } + return nil } diff --git a/go-packages/meep-net-char-mgr/algo-segment.go b/go-packages/meep-net-char-mgr/algo-segment.go index c0827602f0207fd8b992a18b8e0d0916eced7e7a..275dbe715e97410ed82b7cb2c2d813720704d847 100644 --- a/go-packages/meep-net-char-mgr/algo-segment.go +++ b/go-packages/meep-net-char-mgr/algo-segment.go @@ -275,11 +275,14 @@ func (algo *SegmentAlgorithm) ProcessScenario(model *mod.Model, pduSessions map[ } // Create all flows using Network Element list + validFlows := make(map[string]bool) for _, elemSrc := range netElemList { for _, elemDest := range netElemList { if elemSrc.Name != elemDest.Name { + flowName := elemSrc.Name + ":" + elemDest.Name // Create flow - algo.populateFlow(elemSrc.Name+":"+elemDest.Name, &elemSrc, &elemDest, netElemList, 0, model, pduSessions, d2dSessions) + algo.populateFlow(flowName, &elemSrc, &elemDest, netElemList, 0, model, pduSessions, d2dSessions) + validFlows[flowName] = true // Create DB entry to begin collecting metrics for this flow algo.createMetricsEntry(elemSrc.Name, elemDest.Name) @@ -287,6 +290,25 @@ func (algo *SegmentAlgorithm) ProcessScenario(model *mod.Model, pduSessions map[ } } + // Remove flows that are no longer valid + for flowName, flow := range algo.FlowMap { + if !validFlows[flowName] { + delete(algo.FlowMap, flowName) + log.Debug("Flow removed: ", flowName) + // Check if dstElem still has other valid incoming flows; if not, delete its metrics entry + dstStillValid := false + for vFlowName := range validFlows { + if strings.HasSuffix(vFlowName, ":"+flow.DstNetElem) { + dstStillValid = true + break + } + } + if !dstStillValid { + _ = algo.rc.DelEntry(algo.BaseKey + flow.DstNetElem + ":throughput") + } + } + } + // Log segments & flows in Verbose mode if algo.Config.LogVerbose { log.Info("Segments map: ", algo.SegmentMap) diff --git a/go-packages/meep-sessions/session-store.go b/go-packages/meep-sessions/session-store.go index e4c1fad13b0894cba52f4b279327856d56801437..3535fe0e7ee9c222ecb63bc9611e7059935e3e96 100644 --- a/go-packages/meep-sessions/session-store.go +++ b/go-packages/meep-sessions/session-store.go @@ -21,6 +21,7 @@ import ( "net/http" "os" "strings" + "sync" "time" dkm "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-key-mgr" @@ -67,6 +68,7 @@ type SessionStore struct { rc *redis.Connector cs *sessions.CookieStore baseKey string + cache sync.Map } // NewSessionStore - Create and initialize a Session Store instance @@ -94,12 +96,17 @@ func NewSessionStore(addr string) (ss *SessionStore, err error) { log.Info("Connected to Session Store Redis DB") // Create Cookie store + secureCookie := true + if os.Getenv("MEEP_HTTPS_ONLY") == "false" { + secureCookie = false + } + ss.cs = sessions.NewCookieStore([]byte(sessionKey)) ss.cs.Options = &sessions.Options{ Path: "/", MaxAge: SessionDuration, // 20 minutes HttpOnly: true, - Secure: true, + Secure: secureCookie, SameSite: http.SameSiteLaxMode, } log.Info("Created Cookie Store") @@ -123,8 +130,13 @@ func (ss *SessionStore) Get(r *http.Request) (s *Session, err error) { return nil, err } - // Get session from DB + // Get session from DB or Cache sessionId := sessionCookie.Values[ValSessionID].(string) + + if cachedSession, ok := ss.cache.Load(sessionId); ok { + return cachedSession.(*Session), nil + } + session, err := ss.rc.GetEntry(ss.baseKey + sessionId) if err != nil { return nil, err @@ -142,16 +154,18 @@ func (ss *SessionStore) Get(r *http.Request) (s *Session, err error) { s.Role = session[ValRole] s.Timestamp, _ = time.Parse(time.RFC3339, session[ValTimestamp]) s.StartTime, _ = time.Parse(time.RFC3339, session[ValStartTime]) + + ss.cache.Store(sessionId, s) return s, nil } // GetCount - Retrieve session count func (ss *SessionStore) GetCount() (count int) { - _ = ss.rc.ForEachEntry(ss.baseKey+"*", getCountHandler, &count) + _ = ss.rc.ForEachKey(ss.baseKey+"*", getCountKeyHandler, &count) return count } -func getCountHandler(key string, fields map[string]string, userData interface{}) error { +func getCountKeyHandler(key string, userData interface{}) error { count := userData.(*int) *count += 1 return nil @@ -190,7 +204,7 @@ func (ss *SessionStore) GetByName(provider string, username string) (s *Session, s.Username = username s.Provider = provider err = ss.rc.ForEachEntry(ss.baseKey+"*", getUserEntryHandler, s) - if err != nil { + if err != nil && err.Error() != "FOUND" { return nil, err } @@ -204,11 +218,6 @@ func (ss *SessionStore) GetByName(provider string, username string) (s *Session, func getUserEntryHandler(key string, fields map[string]string, userData interface{}) error { s := userData.(*Session) - // Check if session already found - if s.ID != "" { - return nil - } - // look for matching username if fields[ValUsername] == s.Username && fields[ValProvider] == s.Provider { s.ID = fields[ValSessionID] @@ -216,6 +225,7 @@ func getUserEntryHandler(key string, fields map[string]string, userData interfac s.Role = fields[ValRole] s.Timestamp, _ = time.Parse(time.RFC3339, fields[ValTimestamp]) s.StartTime, _ = time.Parse(time.RFC3339, fields[ValStartTime]) + return errors.New("FOUND") } return nil } @@ -257,6 +267,12 @@ func (ss *SessionStore) Set(s *Session, w http.ResponseWriter, r *http.Request) return err, http.StatusInternalServerError } + // Update cache + s.ID = sessionId + s.Timestamp, _ = time.Parse(time.RFC3339, fields[ValTimestamp].(string)) + s.StartTime = sessionStartTime + ss.cache.Store(sessionId, s) + // Update session cookie sessionCookie.Values[ValSessionID] = sessionId err = sessionCookie.Save(r, w) @@ -284,6 +300,7 @@ func (ss *SessionStore) Del(w http.ResponseWriter, r *http.Request) (err error, if err != nil { log.Error("Failed to delete entry for ", sessionId, " with err: ", err.Error()) } + ss.cache.Delete(sessionId) // Delete session cookie sessionCookie.Values[ValSessionID] = "" @@ -303,6 +320,7 @@ func (ss *SessionStore) DelById(sessionId string) error { log.Error("Failed to delete entry for ", sessionId, " with err: ", err.Error()) return err } + ss.cache.Delete(sessionId) return nil } diff --git a/go-packages/meep-users/db.go b/go-packages/meep-users/db.go index cce456cbb672621d721a027d298f6fa9e722a539..fa0ae2914b24a506b86f3a69961eae8f8f21ece8 100644 --- a/go-packages/meep-users/db.go +++ b/go-packages/meep-users/db.go @@ -454,7 +454,7 @@ func (pc *Connector) AuthenticateUser(provider string, username string, password SELECT id FROM `+UsersTable+` WHERE provider = ($1) AND username = ($2) - AND password = crypt('`+password+`', password)`, provider, username) + AND password = crypt($3, password)`, provider, username, password) if err != nil { log.Error(err.Error()) return false, err diff --git a/js-apps/meep-admin-console/src/js/containers/cfg/cfg-network-element-container.js b/js-apps/meep-admin-console/src/js/containers/cfg/cfg-network-element-container.js index c31f636a1f49a0a456dab40c131b6d9b0f3fbe2e..3c2c3547e5975f750ec7b78521fc2aeabf09ec65 100644 --- a/js-apps/meep-admin-console/src/js/containers/cfg/cfg-network-element-container.js +++ b/js-apps/meep-admin-console/src/js/containers/cfg/cfg-network-element-container.js @@ -464,6 +464,7 @@ const DependenciesGroup = ({ onUpdate, element, tableData }) => { const isMepParent = parent && parent.startsWith('mep'); let edgeApps = []; + let cloudApps = []; if (tableData) { edgeApps = Object.values(tableData).filter(e => { const type = getElemFieldVal(e, FIELD_TYPE); @@ -478,6 +479,16 @@ const DependenciesGroup = ({ onUpdate, element, tableData }) => { } return false; }); + + cloudApps = Object.values(tableData).filter(e => { + const type = getElemFieldVal(e, FIELD_TYPE); + const name = getElemFieldVal(e, FIELD_NAME); + + if (type === ELEMENT_TYPE_CLOUD_APP && name !== getElemFieldVal(element, FIELD_NAME)) { + return true; + } + return false; + }); } const handleCheckboxChange = (appName, checked) => { @@ -494,28 +505,61 @@ const DependenciesGroup = ({ onUpdate, element, tableData }) => { return (
- Dependencies (Edge Applications) - {edgeApps.length === 0 ? ( -
No valid edge applications found.
- ) : ( -
- - {edgeApps.map(app => { - const appName = getElemFieldVal(app, FIELD_NAME); - return ( - - handleCheckboxChange(appName, e.target.checked)} - > - {appName} - - - ); - })} - -
- )} + Dependencies + +
+ {/* Edge Applications Column */} +
+ Edge Applications + {edgeApps.length === 0 ? ( +
No valid edge applications found.
+ ) : ( +
+ + {edgeApps.map(app => { + const appName = getElemFieldVal(app, FIELD_NAME); + return ( + + handleCheckboxChange(appName, e.target.checked)} + > + {appName} + + + ); + })} + +
+ )} +
+ + {/* Cloud Applications Column */} +
+ Cloud Applications + {cloudApps.length === 0 ? ( +
No valid cloud applications found.
+ ) : ( +
+ + {cloudApps.map(app => { + const appName = getElemFieldVal(app, FIELD_NAME); + return ( + + handleCheckboxChange(appName, e.target.checked)} + > + {appName} + + + ); + })} + +
+ )} +
+
); }; diff --git a/js-apps/meep-admin-console/src/js/util/scenario-utils.js b/js-apps/meep-admin-console/src/js/util/scenario-utils.js index 0780c49c843fa931f08a7c76a6bc922320419eb5..ef978e67212deaee57fa47d21170b1643fdfe761 100644 --- a/js-apps/meep-admin-console/src/js/util/scenario-utils.js +++ b/js-apps/meep-admin-console/src/js/util/scenario-utils.js @@ -739,6 +739,9 @@ export function updateElementInScenario(scenario, element) { for (var m in pl.processes) { var process = pl.processes[m]; if (process.id === id) { + if (process.name !== name) { + renameProcessDependencies(scenario, process.name, name); + } pl.processes[m] = createProcess( process.id, name, @@ -2302,3 +2305,38 @@ export const getElementNames = (neType, scenario) => { return elementNames; }; + +export const renameProcessDependencies = (scenario, oldName, newName) => { + if (oldName === newName || !scenario || !scenario.deployment || !scenario.deployment.domains) { + return; + } + for (var i in scenario.deployment.domains) { + var domain = scenario.deployment.domains[i]; + if (domain.zones) { + for (var j in domain.zones) { + var zone = domain.zones[j]; + if (zone.networkLocations) { + for (var k in zone.networkLocations) { + var nl = zone.networkLocations[k]; + if (nl.physicalLocations) { + for (var l in nl.physicalLocations) { + var pl = nl.physicalLocations[l]; + if (pl.processes) { + for (var m in pl.processes) { + var proc = pl.processes[m]; + if (proc.dependencies) { + var idx = proc.dependencies.indexOf(oldName); + if (idx !== -1) { + proc.dependencies[idx] = newName; + } + } + } + } + } + } + } + } + } + } + } +}; diff --git a/scripts/meep-cluster-clean.sh b/scripts/meep-cluster-clean.sh new file mode 100755 index 0000000000000000000000000000000000000000..1785080600b5f83e885237ab894f30ca7a84b7e0 --- /dev/null +++ b/scripts/meep-cluster-clean.sh @@ -0,0 +1,355 @@ +#!/usr/bin/env bash +# +# kubeadm-clean.sh +# +# Completely cleans a kubeadm node for re-initialization. +# WARNING: This deletes the Kubernetes cluster state on this node. +# +# Usage: +# sudo bash kubeadm-clean.sh +# + +set -Eeuo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[ OK ]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +err() { echo -e "${RED}[FAIL]${NC} $*"; } + +[[ $EUID -eq 0 ]] || { + err "Run as root." + exit 1 +} + +echo +warn "This will completely remove Kubernetes and CNI state." +sleep 3 + +############################################################################### +# MEEP Cleanup +############################################################################### + +log "Resolving user for MEEP cleanup..." +if [ -n "${SUDO_USER:-}" ]; then + REAL_USER="$SUDO_USER" + REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6) +else + REAL_USER="root" + REAL_HOME="/root" +fi + +log "Running meepctl delete core and dep as $REAL_USER..." +sudo -u "$REAL_USER" bash -c "unset SUDO_UID SUDO_GID SUDO_USER; cd $REAL_HOME/etsi-mec-sandbox && ./bin/meepctl/meepctl delete core" || true +sudo -u "$REAL_USER" bash -c "unset SUDO_UID SUDO_GID SUDO_USER; cd $REAL_HOME/etsi-mec-sandbox && ./bin/meepctl/meepctl delete dep" || true + +log "Deleting MEEP configurations and workspace..." +rm -rf "$REAL_HOME/etsi-mec-sandbox/bin/meepctl" +rm -rf "$REAL_HOME/.meep" +rm -rf "$REAL_HOME/gocode" + +############################################################################### +# Delete Deployments +############################################################################### + +log "Deleting all deployments in default and calico-system namespaces..." +timeout 10s kubectl --kubeconfig=/etc/kubernetes/admin.conf delete all --all -n default --force --grace-period=0 || true +timeout 10s kubectl --kubeconfig=/etc/kubernetes/admin.conf delete all --all -n calico-system --force --grace-period=0 || true + +############################################################################### +# Flush Images +############################################################################### + +log "Flushing Docker images..." +if command -v docker >/dev/null; then + docker rmi -f $(docker images -aq) 2>/dev/null || true +fi + +log "Flushing Containerd images..." +if command -v crictl >/dev/null; then + crictl rmi --all 2>/dev/null || true +fi + +############################################################################### +# kubeadm reset +############################################################################### + +if command -v kubeadm >/dev/null; then + log "Running kubeadm reset..." + timeout 30s kubeadm reset -f || true +fi + +############################################################################### +# Stop services and forcefully kill lingering port bindings +############################################################################### + +log "Stopping services..." + +systemctl stop kubelet 2>/dev/null || true +systemctl stop containerd 2>/dev/null || true +systemctl stop cri-o 2>/dev/null || true + +log "Forcefully killing lingering Kubernetes processes and port bindings..." +pkill -9 -f "kube-apiserver|etcd|kube-controller-manager|kube-scheduler|kubelet|containerd-shim" || true +fuser -k -9 6443/tcp 2>/dev/null || true +fuser -k -9 10250/tcp 2>/dev/null || true +fuser -k -9 2379/tcp 2>/dev/null || true + +############################################################################### +# Uninstall Docker and Containerd +############################################################################### + +log "Uninstalling Docker and Containerd..." +apt-get purge -y --allow-change-held-packages docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras >/dev/null 2>&1 || true +apt-get autoremove -y --purge >/dev/null 2>&1 || true +rm -rf /var/lib/docker >/dev/null 2>&1 || true +rm -rf /var/lib/containerd >/dev/null 2>&1 || true + +############################################################################### +# Uninstall Developer Tools (Golang, Node/NVM, Helm) +############################################################################### + +log "Uninstalling Developer Tools (Golang, NVM, Helm)..." +rm -rf /usr/local/go >/dev/null 2>&1 || true +rm -rf "$REAL_HOME/.nvm" >/dev/null 2>&1 || true +if command -v snap >/dev/null; then + snap remove helm >/dev/null 2>&1 || true +fi + +############################################################################### +# Revert System and Profile Configurations +############################################################################### + +log "Reverting system configurations..." +# Remove meep-docker-registry from hosts +sed -i '/meep-docker-registry/d' /etc/hosts || true + +# Remove kernel modules load config +rm -f /etc/modules-load.d/k8s.conf >/dev/null 2>&1 || true + +log "Reverting user profile (.bashrc)..." +if [ -f "$REAL_HOME/.bashrc" ]; then + sed -i '/PYINFRA MANAGED - Go environment setup/d' "$REAL_HOME/.bashrc" || true + sed -i '/export GOPATH/d' "$REAL_HOME/.bashrc" || true + sed -i '/export PATH.*GOPATH/d' "$REAL_HOME/.bashrc" || true + sed -i '/kubectl completion bash/d' "$REAL_HOME/.bashrc" || true +fi + +############################################################################### +# Remove Kubernetes directories +############################################################################### + +log "Force unmounting lingering kubelet mounts..." +for mount in $(mount | grep '/var/lib/kubelet' | awk '{print $3}'); do + umount -f "$mount" 2>/dev/null || true + umount -l "$mount" 2>/dev/null || true +done + +log "Removing Kubernetes files..." + +rm -rf \ + /etc/kubernetes \ + /var/lib/etcd \ + /var/lib/kubelet \ + /var/lib/cni \ + /var/lib/calico \ + /etc/cni/net.d \ + /var/run/calico \ + ~/.kube >/dev/null 2>&1 || true + +############################################################################### +# Remove CNI interfaces +############################################################################### + +log "Removing CNI interfaces..." + +for iface in $(ip -o link | awk -F': ' '{print $2}' | sed 's/@.*//'); do + case "$iface" in + cali*|cni*|flannel*|vxlan.calico|vxlan-v6.calico|bpfin.cali|bpfout.cali) + ip link delete "$iface" 2>/dev/null || true + ;; + esac +done + +############################################################################### +# Remove network namespaces +############################################################################### + +log "Removing CNI namespaces..." + +ip netns | awk '{print $1}' | while read -r ns; do + ip netns delete "$ns" 2>/dev/null || true +done + +############################################################################### +# Flush iptables +############################################################################### + +log "Flushing iptables..." + +for table in filter nat mangle raw security; do + iptables -t "$table" -F 2>/dev/null || true + iptables -t "$table" -X 2>/dev/null || true +done + +ip6tables -F 2>/dev/null || true +ip6tables -X 2>/dev/null || true + +############################################################################### +# Remove IPVS +############################################################################### + +if command -v ipvsadm >/dev/null; then + log "Clearing IPVS..." + ipvsadm --clear || true +fi + +############################################################################### +# Cleanup containerd +############################################################################### + +if command -v crictl >/dev/null; then + log "Cleaning CRI..." + + PODS=$(crictl pods -q 2>/dev/null || true) + if [ -n "$PODS" ]; then + crictl stopp $PODS >/dev/null 2>&1 || true + crictl rmp $PODS >/dev/null 2>&1 || true + fi + CONTAINERS=$(crictl ps -aq 2>/dev/null || true) + if [ -n "$CONTAINERS" ]; then + crictl rm $CONTAINERS >/dev/null 2>&1 || true + fi +fi + +############################################################################### +# Restart runtime +############################################################################### + +log "Starting runtime..." + +systemctl start containerd 2>/dev/null || true +systemctl start cri-o 2>/dev/null || true + +############################################################################### +# Verification +############################################################################### + +echo +echo "================ Verification ================" + +echo +echo "Kubernetes directories:" +for d in \ + /etc/kubernetes \ + /var/lib/etcd \ + /var/lib/kubelet \ + /etc/cni/net.d \ + /var/lib/calico +do + if [[ -e "$d" ]]; then + warn "$d exists" + else + ok "$d removed" + fi +done + +echo +echo "Developer Tools & Configurations:" +for d in \ + "$REAL_HOME/.meep" \ + "$REAL_HOME/gocode" \ + "$REAL_HOME/.nvm" \ + /usr/local/go \ + /etc/modules-load.d/k8s.conf +do + if [[ -e "$d" ]]; then + warn "$d exists" + else + ok "$d removed" + fi +done + +if command -v helm >/dev/null; then + warn "helm is still installed" +else + ok "helm removed" +fi + +if grep -q "meep-docker-registry" /etc/hosts; then + warn "meep-docker-registry still in /etc/hosts" +else + ok "/etc/hosts cleaned" +fi + +echo +echo "Network interfaces:" +if ip link | grep -E 'cali|cni|flannel|vxlan|bpf' >/dev/null; then + warn "Residual interfaces detected:" + ip link | grep -E 'cali|cni|flannel|vxlan|bpf' +else + ok "No Kubernetes interfaces found." +fi + +echo +echo "Network namespaces:" +if ip netns | grep -q .; then + warn "Residual namespaces:" + ip netns +else + ok "No CNI namespaces." +fi + +echo +echo "CRI containers:" +if command -v crictl >/dev/null; then + if crictl ps -a -q 2>/dev/null | grep -q .; then + warn "Residual containers:" + crictl ps -a 2>/dev/null + else + ok "No containers." + fi + + echo + + if crictl pods -q 2>/dev/null | grep -q .; then + warn "Residual pod sandboxes:" + crictl pods 2>/dev/null + else + ok "No pod sandboxes." + fi +fi + +echo +echo "Kubelet:" +if systemctl is-active --quiet kubelet; then + warn "kubelet is running." +else + ok "kubelet stopped." +fi + +echo +echo "Container runtime:" +if systemctl is-active --quiet containerd; then + ok "containerd running." +elif systemctl is-active --quiet cri-o; then + ok "CRI-O running." +else + warn "No container runtime running." +fi + +echo +echo "==============================================" + +ok "Cleanup complete." + +echo +echo "Recommended:" +echo "1. Reboot the node." +echo "2. Run kubeadm init." +echo "3. Install the CNI plugin." \ No newline at end of file