Parcourir la source

Merge branch 'develop' into opencost-workflow-fix

Signed-off-by: Cliff Colvin <ccolvin@kubecost.com>
Cliff Colvin il y a 2 ans
Parent
commit
e8b42c86fc

+ 9 - 8
.github/workflows/build-and-publish-release.yml

@@ -22,8 +22,8 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     permissions:
     permissions:
       contents: read
       contents: read
-      id-token: write
-    
+      packages: write
+
     steps:
     steps:
       - name: Show Input Values
       - name: Show Input Values
         run: |
         run: |
@@ -34,13 +34,13 @@ jobs:
         run: |
         run: |
           VERSION_NUMBER=${{ inputs.release_version }}
           VERSION_NUMBER=${{ inputs.release_version }}
           echo "BRANCH_NAME=v${VERSION_NUMBER%.*}" >> $GITHUB_ENV
           echo "BRANCH_NAME=v${VERSION_NUMBER%.*}" >> $GITHUB_ENV
-  
+
       - name: Checkout Repo
       - name: Checkout Repo
         uses: actions/checkout@v4
         uses: actions/checkout@v4
         with:
         with:
           repository: 'opencost/opencost'
           repository: 'opencost/opencost'
           ref: '${{ steps.branch.outputs.BRANCH_NAME }}'
           ref: '${{ steps.branch.outputs.BRANCH_NAME }}'
-          path: ./opencost  
+          path: ./opencost
 
 
       - name: Set SHA
       - name: Set SHA
         id: sha
         id: sha
@@ -74,13 +74,15 @@ jobs:
         #  echo "IMAGE_TAG_UI_QUAY=quay.io/kubecost1/opencost-ui:${{ steps.sha.outputs.OC_SHORTHASH }}" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_UI_QUAY=quay.io/kubecost1/opencost-ui:${{ steps.sha.outputs.OC_SHORTHASH }}" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_UI_LATEST_QUAY=quay.io/kubecost1/opencost-ui:latest" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_UI_LATEST_QUAY=quay.io/kubecost1/opencost-ui:latest" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_UI_VERSION_QUAY=quay.io/kubecost1/opencost-ui:prod-${{ inputs.release_version }}" >> $GITHUB_OUTPUT
         #  echo "IMAGE_TAG_UI_VERSION_QUAY=quay.io/kubecost1/opencost-ui:prod-${{ inputs.release_version }}" >> $GITHUB_OUTPUT
-     
+
       - name: Set up Docker Buildx
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v3
         uses: docker/setup-buildx-action@v3
-      
+        with:
+          buildkitd-flags: --debug
+
       - name: Set up just
       - name: Set up just
         uses: extractions/setup-just@v1
         uses: extractions/setup-just@v1
-      
+
       - name: Install crane
       - name: Install crane
         uses: imjasonh/setup-crane@v0.1
         uses: imjasonh/setup-crane@v0.1
 
 
@@ -106,7 +108,6 @@ jobs:
 #          registry: quay.io
 #          registry: quay.io
 #          username: ${{ secrets.QUAY_USERNAME }}
 #          username: ${{ secrets.QUAY_USERNAME }}
 #          password: ${{ secrets.QUAY_PASSWORD }}
 #          password: ${{ secrets.QUAY_PASSWORD }}
-
       - name: Build and push (multiarch) OpenCost
       - name: Build and push (multiarch) OpenCost
         working-directory: ./opencost
         working-directory: ./opencost
         run: |
         run: |

+ 13 - 12
go.mod

@@ -10,10 +10,10 @@ require (
 	cloud.google.com/go/compute/metadata v0.2.3
 	cloud.google.com/go/compute/metadata v0.2.3
 	cloud.google.com/go/storage v1.30.1
 	cloud.google.com/go/storage v1.30.1
 	github.com/Azure/azure-pipeline-go v0.2.3
 	github.com/Azure/azure-pipeline-go v0.2.3
-	github.com/Azure/azure-sdk-for-go v65.0.0+incompatible
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0
-	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2
-	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0
+	github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
+	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.0
 	github.com/Azure/azure-storage-blob-go v0.15.0
 	github.com/Azure/azure-storage-blob-go v0.15.0
 	github.com/Azure/go-autorest/autorest v0.11.28
 	github.com/Azure/go-autorest/autorest v0.11.28
 	github.com/Azure/go-autorest/autorest/adal v0.9.21
 	github.com/Azure/go-autorest/autorest/adal v0.9.21
@@ -31,7 +31,7 @@ require (
 	github.com/aws/smithy-go v1.19.0
 	github.com/aws/smithy-go v1.19.0
 	github.com/davecgh/go-spew v1.1.1
 	github.com/davecgh/go-spew v1.1.1
 	github.com/getsentry/sentry-go v0.25.0
 	github.com/getsentry/sentry-go v0.25.0
-	github.com/google/uuid v1.4.0
+	github.com/google/uuid v1.6.0
 	github.com/jszwec/csvutil v1.2.1
 	github.com/jszwec/csvutil v1.2.1
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/kubecost/events v0.0.6
 	github.com/kubecost/events v0.0.6
@@ -68,7 +68,7 @@ require (
 	cloud.google.com/go v0.110.10 // indirect
 	cloud.google.com/go v0.110.10 // indirect
 	cloud.google.com/go/compute v1.23.3 // indirect
 	cloud.google.com/go/compute v1.23.3 // indirect
 	cloud.google.com/go/iam v1.1.5 // indirect
 	cloud.google.com/go/iam v1.1.5 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
 	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
 	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
@@ -76,7 +76,7 @@ require (
 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
-	github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
 	github.com/PuerkitoBio/purell v1.1.1 // indirect
 	github.com/PuerkitoBio/purell v1.1.1 // indirect
 	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
 	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
 	github.com/andybalholm/brotli v1.0.5 // indirect
 	github.com/andybalholm/brotli v1.0.5 // indirect
@@ -111,6 +111,7 @@ require (
 	github.com/gofrs/uuid v4.2.0+incompatible // indirect
 	github.com/gofrs/uuid v4.2.0+incompatible // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
@@ -150,7 +151,7 @@ require (
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
 	github.com/pelletier/go-toml v1.9.3 // indirect
 	github.com/pelletier/go-toml v1.9.3 // indirect
 	github.com/pierrec/lz4/v4 v4.1.15 // indirect
 	github.com/pierrec/lz4/v4 v4.1.15 // indirect
-	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/procfs v0.11.1 // indirect
 	github.com/prometheus/procfs v0.11.1 // indirect
 	github.com/rs/xid v1.4.0 // indirect
 	github.com/rs/xid v1.4.0 // indirect
@@ -168,11 +169,11 @@ require (
 	go.opentelemetry.io/otel/metric v1.21.0 // indirect
 	go.opentelemetry.io/otel/metric v1.21.0 // indirect
 	go.opentelemetry.io/otel/trace v1.21.0 // indirect
 	go.opentelemetry.io/otel/trace v1.21.0 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
-	golang.org/x/crypto v0.17.0 // indirect
+	golang.org/x/crypto v0.18.0 // indirect
 	golang.org/x/mod v0.10.0 // indirect
 	golang.org/x/mod v0.10.0 // indirect
-	golang.org/x/net v0.19.0 // indirect
-	golang.org/x/sys v0.15.0 // indirect
-	golang.org/x/term v0.15.0 // indirect
+	golang.org/x/net v0.20.0 // indirect
+	golang.org/x/sys v0.16.0 // indirect
+	golang.org/x/term v0.16.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/tools v0.9.1 // indirect
 	golang.org/x/tools v0.9.1 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect

+ 29 - 25
go.sum

@@ -55,16 +55,18 @@ cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7Biccwk
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
-github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw=
-github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0 h1:xGLAFFd9D3iLGxYiUGPdITSzsFmU1K8VtfuUHWAoN7M=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 h1:uqM+VoHjVH6zdlkLF2b6O0ZANcHoj3rO0PoQ3jglUJA=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
-github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY=
-github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag=
+github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
+github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 h1:c4k2FIYIh4xtwqrQwV0Ct1v5+ehlNXj5NI/MWVsiTkQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2/go.mod h1:5FDJtLEO/GxwNgUxbwrY3LP0pEoThTQJtk2oysdXHxM=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.0 h1:IfFdxTUDiV58iZqPKgyWiz4X4fCxZeQ1pTQPImLYXpY=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.0/go.mod h1:SUZc9YRRHfx2+FAQKNDGrssXehqLpxmwRv2mC/5ntj4=
 github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
 github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
 github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
 github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
@@ -93,8 +95,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
 github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
-github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0 h1:UE9n9rkJF62ArLb1F3DEjRt8O3jLwMWdSoypKV4f3MU=
-github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU=
 github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU=
@@ -249,6 +251,8 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
+github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -329,8 +333,8 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
 github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
 github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -482,8 +486,8 @@ github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0
 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
-github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
-github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -610,8 +614,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -692,8 +696,8 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1
 golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
-golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -766,16 +770,16 @@ golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
-golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

+ 1 - 1
pkg/cloud/azure/storageauthorizer.go

@@ -116,7 +116,7 @@ func (ah *AuthorizerHolder) GetBlobClient(serviceURL string) (*azblob.Client, er
 	// Create a default request pipeline using your storage account name and account key.
 	// Create a default request pipeline using your storage account name and account key.
 	cred, err := ah.GetCredential()
 	cred, err := ah.GetCredential()
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("error retrieving credentials: %w", err)
 	}
 	}
 
 
 	client, err := azblob.NewClient(serviceURL, cred, nil)
 	client, err := azblob.NewClient(serviceURL, cred, nil)

+ 287 - 153
pkg/cloud/config/controller.go

@@ -2,9 +2,13 @@ package config
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"os"
+	"path/filepath"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/util/json"
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 	"github.com/opencost/opencost/core/pkg/util/timeutil"
 	"github.com/opencost/opencost/pkg/cloud"
 	"github.com/opencost/opencost/pkg/cloud"
 	"github.com/opencost/opencost/pkg/cloud/models"
 	"github.com/opencost/opencost/pkg/cloud/models"
@@ -12,36 +16,14 @@ import (
 	"github.com/opencost/opencost/pkg/env"
 	"github.com/opencost/opencost/pkg/env"
 )
 )
 
 
-// configID identifies the source and the ID of a configuration to handle duplicate configs from multiple sources
-type configID struct {
-	source ConfigSource
-	key    string
-}
-
-func (cid configID) Equals(that configID) bool {
-	return cid.source == that.source && cid.key == that.key
-}
-
-func newConfigID(source, key string) configID {
-	return configID{
-		source: GetConfigSource(source),
-		key:    key,
-	}
-}
-
-type Status struct {
-	Source ConfigSource
-	Key    string
-	Active bool
-	Valid  bool
-	Config cloud.KeyedConfig
-}
+const configFile = "cloud-configurations.json"
 
 
 // Controller manages the cloud.Config using config Watcher(s) to track various configuration
 // Controller manages the cloud.Config using config Watcher(s) to track various configuration
 // methods. To do this it has a map of config watchers mapped on configuration source and a list Observers that it updates
 // methods. To do this it has a map of config watchers mapped on configuration source and a list Observers that it updates
 // upon any change detected from the config watchers.
 // upon any change detected from the config watchers.
 type Controller struct {
 type Controller struct {
-	statuses  map[configID]*Status
+	path      string
+	lock      sync.RWMutex
 	observers []Observer
 	observers []Observer
 	watchers  map[ConfigSource]cloud.KeyedConfigWatcher
 	watchers  map[ConfigSource]cloud.KeyedConfigWatcher
 }
 }
@@ -56,11 +38,10 @@ func NewController(cp models.Provider) *Controller {
 		watchers = GetCloudBillingWatchers(nil)
 		watchers = GetCloudBillingWatchers(nil)
 	}
 	}
 	ic := &Controller{
 	ic := &Controller{
-		statuses: make(map[configID]*Status),
+		path:     filepath.Join(env.GetConfigPathWithDefault(env.DefaultConfigMountPath), configFile),
 		watchers: watchers,
 		watchers: watchers,
 	}
 	}
 
 
-	ic.load()
 	ic.pullWatchers()
 	ic.pullWatchers()
 
 
 	go func() {
 	go func() {
@@ -79,38 +60,217 @@ func NewController(cp models.Provider) *Controller {
 	return ic
 	return ic
 }
 }
 
 
-func (c *Controller) EnableConfig(key, source string) error {
-	cID := newConfigID(source, key)
-	cs, ok := c.statuses[cID]
+// pullWatchers retrieve configs from watchers and update configs according to priority of sources
+func (c *Controller) pullWatchers() {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	statuses, err := c.load()
+	if err != nil {
+		log.Errorf("failed to load statuses when pulling watchers %s", err)
+		statuses = Statuses{}
+	}
+	for source, watcher := range c.watchers {
+		watcherConfsByKey := map[string]cloud.KeyedConfig{}
+		for _, wConf := range watcher.GetConfigs() {
+			watcherConfsByKey[wConf.Key()] = wConf
+		}
+
+		// remove existing configs that are no longer present in the source
+		for _, status := range statuses.List() {
+			if status.Source == source {
+				if _, ok := watcherConfsByKey[status.Key]; !ok {
+					err := c.deleteConfig(status.Key, status.Source, statuses)
+					if err != nil {
+						log.Errorf("Conrtoller: pullWatchers: %s", err.Error())
+					}
+				}
+
+			}
+		}
+
+		for key, conf := range watcherConfsByKey {
+
+			// Check existing configs for matching key and source
+			if existingStatus, ok := statuses.Get(key, source); ok {
+				// if config has not changed continue
+				if existingStatus.Config.Equals(conf) {
+					continue
+				}
+				// remove the existing config
+				err := c.deleteConfig(key, source, statuses)
+				if err != nil {
+					log.Errorf("Conrtoller: pullWatchers: %s", err.Error())
+				}
+
+			}
+
+			err := conf.Validate()
+			valid := err == nil
+
+			configType, err := ConfigTypeFromConfig(conf)
+			if err != nil {
+				log.Errorf("failed to get config type for config with key: %s", conf.Key())
+				continue
+			}
+
+			status := Status{
+				Key:        key,
+				Source:     source,
+				Active:     valid, // if valid, then new config will be active
+				Valid:      valid,
+				ConfigType: configType,
+				Config:     conf,
+			}
+
+			// handle a config with a new unique key for a source or an update config from a source which was inactive before
+			if valid {
+				for _, matchStat := range statuses.List() {
+					//// skip matching configs
+					//if matchID.Equals(cID) {
+					//	continue
+					//}
+
+					if matchStat.Active {
+						// if source is non-multi-cloud disable all other non-multi-cloud sourced configs
+						if source == HelmSource || source == ConfigFileSource {
+							if matchStat.Source == HelmSource || matchStat.Source == ConfigFileSource {
+								matchStat.Active = false
+								c.broadcastRemoveConfig(matchStat.Key)
+							}
+						}
+
+						// check for configs with the same key that are active
+						if matchStat.Key == key {
+							// If source has higher priority disable other active configs
+							matchStat.Active = false
+							c.broadcastRemoveConfig(matchStat.Key)
+						}
+					}
+				}
+			}
+
+			// update config and put to observers if active
+			statuses.Insert(&status)
+			if status.Active {
+				c.broadcastAddConfig(conf)
+			}
+			err = c.save(statuses)
+			if err != nil {
+				log.Errorf("failed to save statuses %s", err.Error())
+			}
+		}
+	}
+}
+
+// CreateConfig adds a new config to status with a source of ConfigControllerSource
+// It will disable any config with the same key
+// fails if there is an existing config with the same key and source
+func (c *Controller) CreateConfig(conf cloud.KeyedConfig) error {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	err := conf.Validate()
+	if err != nil {
+		return fmt.Errorf("provided configuration was invalid: %w", err)
+	}
+
+	statuses, err := c.load()
+	if err != nil {
+		return fmt.Errorf("failed to load statuses")
+	}
+	source := ConfigControllerSource
+	key := conf.Key()
+
+	_, ok := statuses.Get(key, source)
+	if ok {
+		return fmt.Errorf("config with key %s from source %s already exist", key, source.String())
+	}
+
+	configType, err := ConfigTypeFromConfig(conf)
+	if err != nil {
+		return fmt.Errorf("config did not have recoginzed config: %w", err)
+	}
+
+	statuses.Insert(&Status{
+		Key:        key,
+		Source:     source,
+		Valid:      true,
+		Active:     true,
+		ConfigType: configType,
+		Config:     conf,
+	})
+
+	// check for configurations with the same configuration key that are already active.
+	for _, confStat := range statuses.List() {
+		if confStat.Key != key || confStat.Source == source {
+			continue
+		}
+
+		// if active disable
+		if confStat.Active == true {
+			confStat.Active = false
+			c.broadcastRemoveConfig(key)
+		}
+
+	}
+
+	c.broadcastAddConfig(conf)
+	err = c.save(statuses)
+	if err != nil {
+		return fmt.Errorf("failed to save statues: %w", err)
+	}
+	return nil
+}
+
+// EnableConfig enables a config with the given key and source, and disables any config with a matching key
+func (c *Controller) EnableConfig(key, sourceStr string) error {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	statuses, err := c.load()
+	if err != nil {
+		return fmt.Errorf("failed to load statuses")
+	}
+
+	source := GetConfigSource(sourceStr)
+	cs, ok := statuses.Get(key, source)
 	if !ok {
 	if !ok {
-		return fmt.Errorf("Controller: EnableConfig: config with key %s from source %s does not exist", key, source)
+		return fmt.Errorf("config with key %s from source %s does not exist", key, sourceStr)
 	}
 	}
 	if cs.Active {
 	if cs.Active {
-		return fmt.Errorf("Controller: EnableConfig: config with key %s from source %s is already active", key, source)
+		return fmt.Errorf("config with key %s from source %s is already active", key, sourceStr)
 	}
 	}
 
 
 	// check for configurations with the same configuration key that are already active.
 	// check for configurations with the same configuration key that are already active.
-	for confID, confStat := range c.statuses {
-		if confID.key != key || confID.source == cID.source {
+	for _, confStat := range statuses.List() {
+		if confStat.Key != key || confStat.Source == source {
 			continue
 			continue
 		}
 		}
 
 
 		// if active disable
 		// if active disable
 		if confStat.Active == true {
 		if confStat.Active == true {
 			confStat.Active = false
 			confStat.Active = false
+			c.broadcastRemoveConfig(key)
 		}
 		}
+
 	}
 	}
 
 
 	cs.Active = true
 	cs.Active = true
-	c.putConfig(cs.Config)
-	c.save()
+	c.broadcastAddConfig(cs.Config)
+	c.save(statuses)
 	return nil
 	return nil
 }
 }
 
 
 // DisableConfig updates an config status if it was enabled
 // DisableConfig updates an config status if it was enabled
-func (c *Controller) DisableConfig(key, source string) error {
-	iID := newConfigID(source, key)
-	is, ok := c.statuses[iID]
+func (c *Controller) DisableConfig(key, sourceStr string) error {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	statuses, err := c.load()
+	if err != nil {
+		return fmt.Errorf("failed to load statuses")
+	}
+	source := GetConfigSource(sourceStr)
+	is, ok := statuses.Get(key, source)
 	if !ok {
 	if !ok {
 		return fmt.Errorf("Controller: DisableConfig: config with key %s from source %s does not exist", key, source)
 		return fmt.Errorf("Controller: DisableConfig: config with key %s from source %s does not exist", key, source)
 	}
 	}
@@ -119,122 +279,84 @@ func (c *Controller) DisableConfig(key, source string) error {
 	}
 	}
 
 
 	is.Active = false
 	is.Active = false
-	c.deleteConfig(iID.key)
-	c.save()
+	c.broadcastRemoveConfig(key)
+	c.save(statuses)
+	return nil
+}
+
+// DeleteConfig removes a config from the statuses and deletes the config on all observers if it was active
+// This can only be used on configs with ConfigControllerSource
+func (c *Controller) DeleteConfig(key, sourceStr string) error {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	source := GetConfigSource(sourceStr)
+	if source != ConfigControllerSource {
+		return fmt.Errorf("controller does not own config with key %s from source %s, manage this config at its source", key, source.String())
+	}
+
+	statuses, err := c.load()
+	if err != nil {
+		return fmt.Errorf("failed to load statuses")
+	}
+
+	err = c.deleteConfig(key, source, statuses)
+	if err != nil {
+		return fmt.Errorf("Controller: DeleteConfig: %w", err)
+	}
 	return nil
 	return nil
 }
 }
 
 
-// DeleteConfig removes an config from the statuses and deletes the config on all observers if it was active
-func (c *Controller) DeleteConfig(key, source string) error {
-	id := newConfigID(source, key)
-	is, ok := c.statuses[id]
+func (c *Controller) deleteConfig(key string, source ConfigSource, statuses Statuses) error {
+	is, ok := statuses.Get(key, source)
 	if !ok {
 	if !ok {
-		return fmt.Errorf("Controller: DisableConfig: config with key %s from source %s does not exist", key, source)
+		return fmt.Errorf("config with key %s from source %s does not exist", key, source.String())
 	}
 	}
 
 
 	// delete config on observers if active
 	// delete config on observers if active
 	if is.Active {
 	if is.Active {
-		c.deleteConfig(id.key)
+		c.broadcastRemoveConfig(key)
 	}
 	}
-	delete(c.statuses, id)
-	c.save()
+	delete(statuses[source], key)
+	c.save(statuses)
 	return nil
 	return nil
 }
 }
 
 
-// pullWatchers retrieve configs from watchers and update configs according to priority of sources
-func (c *Controller) pullWatchers() {
-
-	for source, watcher := range c.watchers {
-		for _, conf := range watcher.GetConfigs() {
-			key := conf.Key()
-			cID := configID{
-				source: source,
-				key:    key,
-			}
-
-			err := conf.Validate()
-			valid := err == nil
-
-			status := Status{
-				Key:    key,
-				Source: source,
-				Active: valid, // active if valid, for now
-				Valid:  valid,
-				Config: conf,
-			}
-
-			// Check existing configs for matching key and source
-			if existingStatus, ok := c.statuses[cID]; ok {
-				// if config has not changed continue
-				if existingStatus.Config.Equals(conf) {
-					continue
-				}
-				// if existing CS is active then it should be replaced by the updated config
-				if existingStatus.Active {
-					if status.Valid {
-						c.putConfig(conf)
-					} else {
-						// if active config is being overwritten by an invalid one, delete the config, as it will not be active
-						c.deleteConfig(key)
-					}
-					c.statuses[cID] = &status
-					continue
-				}
-			}
+func (c *Controller) load() (Statuses, error) {
+	raw, err := os.ReadFile(c.path)
+	if err != nil {
+		return nil, fmt.Errorf("ConfigController: failed to load config statuses from file: %w", err)
+	}
 
 
-			// At this point we know that the config from this watcher has changed
+	statuses := Statuses{}
+	err = json.Unmarshal(raw, &statuses)
+	if err != nil {
+		return nil, fmt.Errorf("ConfigController: failed to marshal config statuses: %s", err.Error())
+	}
 
 
-			// handle an config with a new unique key for a source or an update config from a source which was inactive before
-			if valid {
-				for matchID, matchCS := range c.statuses {
-					// skip matching configs
-					if matchID.Equals(cID) {
-						continue
-					}
+	return statuses, nil
+}
 
 
-					if matchCS.Active {
-						// if source is non-multi-cloud disable all other non-multi-cloud sourced configs
-						if cID.source == HelmSource || cID.source == ConfigFileSource {
-							if matchID.source == HelmSource || matchID.source == ConfigFileSource {
-								matchCS.Active = false
-								c.deleteConfig(matchID.key)
-							}
-						}
+func (c *Controller) save(statuses Statuses) error {
 
 
-						// check for configs with the same key that are active
-						if matchID.key == key {
-							// If source has higher priority disable other active configs
-							matchCS.Active = false
-							c.deleteConfig(matchID.key)
-						}
-					}
-				}
-			}
-
-			// update config and put to observers if active
-			c.statuses[cID] = &status
-			if status.Active {
-				c.putConfig(conf)
-			}
-		}
+	raw, err := json.Marshal(statuses)
+	if err != nil {
+		return fmt.Errorf("ConfigController: failed to marshal config statuses: %s", err)
 	}
 	}
-}
 
 
-// todo implement when building config api and persistence is necessary
-func (c *Controller) load() {}
+	err = os.WriteFile(c.path, raw, 0644)
+	if err != nil {
+		return fmt.Errorf("ConfigController: failed to save config statuses to file: %s", err)
+	}
 
 
-// todo implement when building config api and persistence is necessary
-func (c *Controller) save() {}
+	return nil
+}
 
 
 func (c *Controller) ExportConfigs(key string) (*Configurations, error) {
 func (c *Controller) ExportConfigs(key string) (*Configurations, error) {
+	c.lock.RLock()
+	defer c.lock.RUnlock()
 	configs := new(Configurations)
 	configs := new(Configurations)
 
 
-	activeConfigs := make(map[string]cloud.Config)
-	for iID, cs := range c.statuses {
-		if cs.Active {
-			activeConfigs[iID.key] = cs.Config
-		}
-	}
+	activeConfigs := c.getActiveConfigs()
 	if key != "" {
 	if key != "" {
 		conf, ok := activeConfigs[key]
 		conf, ok := activeConfigs[key]
 		if !ok {
 		if !ok {
@@ -259,17 +381,21 @@ func (c *Controller) ExportConfigs(key string) (*Configurations, error) {
 }
 }
 
 
 func (c *Controller) getActiveConfigs() map[string]cloud.KeyedConfig {
 func (c *Controller) getActiveConfigs() map[string]cloud.KeyedConfig {
-	bi := make(map[string]cloud.KeyedConfig)
-	for iID, cs := range c.statuses {
+	activeConfigs := make(map[string]cloud.KeyedConfig)
+	statuses, err := c.load()
+	if err != nil {
+		log.Errorf("GetStatus: failed to load cloud statuses")
+	}
+	for _, cs := range statuses.List() {
 		if cs.Active {
 		if cs.Active {
-			bi[iID.key] = cs.Config
+			activeConfigs[cs.Key] = cs.Config
 		}
 		}
 	}
 	}
-	return bi
+	return activeConfigs
 }
 }
 
 
-// deleteConfig ask observers to remove and stop all processes related to a configuration with a given key
-func (c *Controller) deleteConfig(key string) {
+// broadcastRemoveConfig ask observers to remove and stop all processes related to a configuration with a given key
+func (c *Controller) broadcastRemoveConfig(key string) {
 	var wg sync.WaitGroup
 	var wg sync.WaitGroup
 	for _, obs := range c.observers {
 	for _, obs := range c.observers {
 		observer := obs
 		observer := obs
@@ -282,30 +408,38 @@ func (c *Controller) deleteConfig(key string) {
 	wg.Wait()
 	wg.Wait()
 }
 }
 
 
+// broadcastAddConfig gives observers a new config to handle
+func (c *Controller) broadcastAddConfig(conf cloud.KeyedConfig) {
+	var wg sync.WaitGroup
+	for _, obs := range c.observers {
+		observer := obs
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			observer.PutConfig(conf)
+		}()
+	}
+	wg.Wait()
+}
+
 // RegisterObserver gives out the current active list configs and adds the observer to the push list
 // RegisterObserver gives out the current active list configs and adds the observer to the push list
 func (c *Controller) RegisterObserver(obs Observer) {
 func (c *Controller) RegisterObserver(obs Observer) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
 	obs.SetConfigs(c.getActiveConfigs())
 	obs.SetConfigs(c.getActiveConfigs())
 	c.observers = append(c.observers, obs)
 	c.observers = append(c.observers, obs)
 }
 }
 
 
 func (c *Controller) GetStatus() []Status {
 func (c *Controller) GetStatus() []Status {
+	c.lock.RLock()
+	defer c.lock.RUnlock()
 	var status []Status
 	var status []Status
-	for _, intStat := range c.statuses {
+	statuses, err := c.load()
+	if err != nil {
+		log.Errorf("GetStatus: failed to load cloud statuses")
+	}
+	for _, intStat := range statuses.List() {
 		status = append(status, *intStat)
 		status = append(status, *intStat)
 	}
 	}
 	return status
 	return status
 }
 }
-
-// putConfig gives observers a new config to handle
-func (c *Controller) putConfig(conf cloud.KeyedConfig) {
-	var wg sync.WaitGroup
-	for _, obs := range c.observers {
-		observer := obs
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
-			observer.PutConfig(conf)
-		}()
-	}
-	wg.Wait()
-}

+ 81 - 23
pkg/cloud/config/controller_handlers.go

@@ -1,12 +1,19 @@
 package config
 package config
 
 
 import (
 import (
+	"bytes"
+	"encoding/json"
 	"fmt"
 	"fmt"
+	"io"
 	"net/http"
 	"net/http"
+	"strings"
 
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/julienschmidt/httprouter"
 	proto "github.com/opencost/opencost/core/pkg/protocol"
 	proto "github.com/opencost/opencost/core/pkg/protocol"
-	"github.com/opencost/opencost/pkg/env"
+	"github.com/opencost/opencost/pkg/cloud"
+	"github.com/opencost/opencost/pkg/cloud/aws"
+	"github.com/opencost/opencost/pkg/cloud/azure"
+	"github.com/opencost/opencost/pkg/cloud/gcp"
 )
 )
 
 
 var protocol = proto.HTTP()
 var protocol = proto.HTTP()
@@ -19,12 +26,6 @@ func (c *Controller) cloudCostChecks() func(w http.ResponseWriter, r *http.Reque
 		}
 		}
 	}
 	}
 
 
-	if !env.IsCloudCostEnabled() {
-		return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
-			http.Error(w, "ConfigController: is not enabled", http.StatusServiceUnavailable)
-		}
-	}
-
 	return nil
 	return nil
 }
 }
 
 
@@ -51,6 +52,77 @@ func (c *Controller) GetExportConfigHandler() func(w http.ResponseWriter, r *htt
 	}
 	}
 }
 }
 
 
+// GetEnableConfigHandler creates a handler from a http request which enables an integration via the integrationController
+func (c *Controller) GetAddConfigHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+	// perform basic checks to ensure that the pipeline can be accessed
+	fn := c.cloudCostChecks()
+	if fn != nil {
+		return fn
+	}
+
+	// Return valid handler func
+	return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+		w.Header().Set("Content-Type", "application/json")
+
+		configType := r.URL.Query().Get("type")
+
+		config, err := parseConfig(configType, r.Body)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+
+		err = c.CreateConfig(config)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+
+		protocol.WriteData(w, fmt.Sprintf("Successfully added integration with key %s", config.Key()))
+	}
+}
+
+func parseConfig(configType string, body io.Reader) (cloud.KeyedConfig, error) {
+	buf := new(bytes.Buffer)
+	_, err := buf.ReadFrom(body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read body: %w", err)
+	}
+	bytes := buf.Bytes()
+	switch strings.ToLower(configType) {
+	case S3ConfigType:
+		config := &aws.S3Configuration{}
+		err = json.Unmarshal(bytes, config)
+		if err != nil {
+			return nil, fmt.Errorf("error unmarshalling S3 Configuration: %w", err)
+		}
+		return config, nil
+	case AthenaConfigType:
+		config := &aws.AthenaConfiguration{}
+		err = json.Unmarshal(bytes, config)
+		if err != nil {
+			return nil, fmt.Errorf("error unmarshalling Athena Configuration: %w", err)
+		}
+		return config, nil
+	case BigQueryConfigType:
+		config := &gcp.BigQueryConfiguration{}
+		err = json.Unmarshal(bytes, config)
+		if err != nil {
+			return nil, fmt.Errorf("error unmarshalling Big Query Configuration: %w", err)
+		}
+		return config, nil
+	case AzureStorageConfigType:
+		config := &azure.StorageConfiguration{}
+		err = json.Unmarshal(bytes, config)
+		if err != nil {
+			return nil, fmt.Errorf("error unmarshalling Azure Storage Configuration: %w", err)
+		}
+		return config, nil
+
+	}
+	return nil, fmt.Errorf("provided config type was not recognised %s", configType)
+}
+
 // GetEnableConfigHandler creates a handler from a http request which enables an integration via the integrationController
 // GetEnableConfigHandler creates a handler from a http request which enables an integration via the integrationController
 func (c *Controller) GetEnableConfigHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
 func (c *Controller) GetEnableConfigHandler() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
 	// perform basic checks to ensure that the pipeline can be accessed
 	// perform basic checks to ensure that the pipeline can be accessed
@@ -136,25 +208,11 @@ func (c *Controller) GetDeleteConfigHandler() func(w http.ResponseWriter, r *htt
 			return
 			return
 		}
 		}
 
 
-		source := r.URL.Query().Get("source")
-		if source == "" {
-			http.Error(w, "required parameter 'source' is missing", http.StatusBadRequest)
-			return
-		}
-
-		err := c.DeleteConfig(integrationKey, source)
+		err := c.DeleteConfig(integrationKey, ConfigControllerSource.String())
 		if err != nil {
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusBadRequest)
 			http.Error(w, err.Error(), http.StatusBadRequest)
 			return
 			return
 		}
 		}
-		protocol.WriteData(w, fmt.Sprintf("Successfully deleted integration with key %s from source %s", integrationKey, source))
-
-		for _, intStat := range c.GetStatus() {
-			if intStat.Key == integrationKey {
-				protocol.WriteData(w, fmt.Sprintf("Found addition integration with integration key %s from source %s. If you wish to delete this data do so manually or delete all integrations with matching keys", integrationKey, intStat.Source))
-				return
-			}
-		}
-		protocol.WriteData(w, fmt.Sprintf("Successfully deleted cloud cost data with key %s", integrationKey))
+		protocol.WriteData(w, fmt.Sprintf("Successfully deleted integration with key %s", integrationKey))
 	}
 	}
 }
 }

+ 863 - 345
pkg/cloud/config/controller_test.go

@@ -1,6 +1,9 @@
 package config
 package config
 
 
 import (
 import (
+	"fmt"
+	"os"
+	"path/filepath"
 	"testing"
 	"testing"
 
 
 	cloudconfig "github.com/opencost/opencost/pkg/cloud"
 	cloudconfig "github.com/opencost/opencost/pkg/cloud"
@@ -67,22 +70,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Helm Source No Change": {
 		"Helm Source No Change": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -94,22 +99,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Helm Source Update Config": {
 		"Helm Source Update Config": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConfModifiedProperty.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConfModifiedProperty,
+					Source:     HelmSource,
+					Key:        validAthenaConfModifiedProperty.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConfModifiedProperty,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -121,22 +128,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Helm Source Update Config Invalid": {
 		"Helm Source Update Config Invalid": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -148,22 +157,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    invalidAthenaConf.Key(),
-					Active: false,
-					Valid:  false,
-					Config: invalidAthenaConf,
+					Source:     HelmSource,
+					Key:        invalidAthenaConf.Key(),
+					Active:     false,
+					Valid:      false,
+					ConfigType: AthenaConfigType,
+					Config:     invalidAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Helm Source New Config": {
 		"Helm Source New Config": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -175,18 +186,12 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: false, // this value changed
-					Valid:  true,
-					Config: validAthenaConf,
-				},
-				{
-					Source: HelmSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     HelmSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 		},
 		},
@@ -202,22 +207,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Config File No Change": {
 		"Config File No Change": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -229,22 +236,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Config File Update Config": {
 		"Config File Update Config": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -256,22 +265,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Config File Update Config Invalid": {
 		"Config File Update Config Invalid": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -283,22 +294,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    invalidAthenaConf.Key(),
-					Active: false,
-					Valid:  false,
-					Config: invalidAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        invalidAthenaConf.Key(),
+					Active:     false,
+					Valid:      false,
+					ConfigType: AthenaConfigType,
+					Config:     invalidAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Config File New Config": {
 		"Config File New Config": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -310,18 +323,12 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: false, // this value changed
-					Valid:  true,
-					Config: validAthenaConf,
-				},
-				{
-					Source: ConfigFileSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 		},
 		},
@@ -337,22 +344,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     MultiCloudSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Multi Cloud No Change": {
 		"Multi Cloud No Change": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     MultiCloudSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -364,22 +373,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     MultiCloudSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Multi Cloud Update Config": {
 		"Multi Cloud Update Config": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     MultiCloudSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -391,22 +402,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validAthenaConfModifiedProperty.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConfModifiedProperty,
+					Source:     MultiCloudSource,
+					Key:        validAthenaConfModifiedProperty.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConfModifiedProperty,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Multi Cloud Update Config Invalid": {
 		"Multi Cloud Update Config Invalid": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     MultiCloudSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -418,22 +431,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    invalidAthenaConf.Key(),
-					Active: false,
-					Valid:  false,
-					Config: invalidAthenaConf,
+					Source:     MultiCloudSource,
+					Key:        invalidAthenaConf.Key(),
+					Active:     false,
+					Valid:      false,
+					ConfigType: AthenaConfigType,
+					Config:     invalidAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Multi Cloud New Config": {
 		"Multi Cloud New Config": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     MultiCloudSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -445,30 +460,41 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     MultiCloudSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
+			},
+		},
+		"Multi Cloud Delete All": {
+			initialStatuses: []*Status{
 				{
 				{
-					Source: MultiCloudSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     MultiCloudSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
+			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
+				MultiCloudSource: &MockKeyedConfigWatcher{},
+			},
+			expectedStatuses: []*Status{},
 		},
 		},
 		// Watch Interaction
 		// Watch Interaction
 		"New Helm, Existing Config File": {
 		"New Helm, Existing Config File": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -485,36 +511,40 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: false,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     false,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 				{
 				{
-					Source: HelmSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     HelmSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Update Helm, Existing Config File": {
 		"Update Helm, Existing Config File": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: false,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     false,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -531,29 +561,32 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConfModifiedProperty.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConfModifiedProperty,
+					Source:     HelmSource,
+					Key:        validAthenaConfModifiedProperty.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConfModifiedProperty,
 				},
 				},
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validBigQueryConf.Key(),
-					Active: false,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     false,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"New Helm Invalid, Existing Config File": {
 		"New Helm Invalid, Existing Config File": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -570,36 +603,40 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 				{
 				{
-					Source: HelmSource,
-					Key:    invalidAthenaConf.Key(),
-					Active: false,
-					Valid:  false,
-					Config: invalidAthenaConf,
+					Source:     HelmSource,
+					Key:        invalidAthenaConf.Key(),
+					Active:     false,
+					Valid:      false,
+					ConfigType: AthenaConfigType,
+					Config:     invalidAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Update Helm Invalid, Existing Config File": {
 		"Update Helm Invalid, Existing Config File": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: false,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     false,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -616,29 +653,32 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 				{
 				{
-					Source: HelmSource,
-					Key:    invalidAthenaConf.Key(),
-					Active: false,
-					Valid:  false,
-					Config: invalidAthenaConf,
+					Source:     HelmSource,
+					Key:        invalidAthenaConf.Key(),
+					Active:     false,
+					Valid:      false,
+					ConfigType: AthenaConfigType,
+					Config:     invalidAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"New Config File, Existing Helm": {
 		"New Config File, Existing Helm": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -655,36 +695,32 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validAthenaConf.Key(),
-					Active: false,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     HelmSource,
+					Key:        validAthenaConf.Key(),
+					Active:     false,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Update Config File, Existing Helm": {
 		"Update Config File, Existing Helm": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: false,
-					Valid:  true,
-					Config: validAthenaConf,
-				},
-				{
-					Source: HelmSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     HelmSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -699,29 +735,24 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConfModifiedProperty.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validAthenaConfModifiedProperty,
-				},
-				{
-					Source: HelmSource,
-					Key:    validBigQueryConf.Key(),
-					Active: false,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConfModifiedProperty.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConfModifiedProperty,
 				},
 				},
 			},
 			},
 		},
 		},
 		"New Config File Invalid, Existing Helm": {
 		"New Config File Invalid, Existing Helm": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     HelmSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -738,36 +769,40 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     HelmSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    invalidAthenaConf.Key(),
-					Active: false,
-					Valid:  false,
-					Config: invalidAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        invalidAthenaConf.Key(),
+					Active:     false,
+					Valid:      false,
+					ConfigType: AthenaConfigType,
+					Config:     invalidAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
 		"Update Config File Invalid, Existing Helm": {
 		"Update Config File Invalid, Existing Helm": {
 			initialStatuses: []*Status{
 			initialStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     HelmSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    validAthenaConf.Key(),
-					Active: false,
-					Valid:  true,
-					Config: validAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        validAthenaConf.Key(),
+					Active:     false,
+					Valid:      true,
+					ConfigType: AthenaConfigType,
+					Config:     validAthenaConf,
 				},
 				},
 			},
 			},
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
 			configWatchers: map[ConfigSource]cloudconfig.KeyedConfigWatcher{
@@ -784,18 +819,20 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 			},
 			},
 			expectedStatuses: []*Status{
 			expectedStatuses: []*Status{
 				{
 				{
-					Source: HelmSource,
-					Key:    validBigQueryConf.Key(),
-					Active: true,
-					Valid:  true,
-					Config: validBigQueryConf,
+					Source:     HelmSource,
+					Key:        validBigQueryConf.Key(),
+					Active:     true,
+					Valid:      true,
+					ConfigType: BigQueryConfigType,
+					Config:     validBigQueryConf,
 				},
 				},
 				{
 				{
-					Source: ConfigFileSource,
-					Key:    invalidAthenaConf.Key(),
-					Active: false,
-					Valid:  false,
-					Config: invalidAthenaConf,
+					Source:     ConfigFileSource,
+					Key:        invalidAthenaConf.Key(),
+					Active:     false,
+					Valid:      false,
+					ConfigType: AthenaConfigType,
+					Config:     invalidAthenaConf,
 				},
 				},
 			},
 			},
 		},
 		},
@@ -804,68 +841,549 @@ func TestIntegrationController_pullWatchers(t *testing.T) {
 	for name, tc := range testCases {
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			// Test set up and validation
 			// Test set up and validation
-			initialStatuses := make(map[configID]*Status)
-			for _, status := range tc.initialStatuses {
-				iID := configID{
-					source: status.Source,
-					key:    status.Key,
-				}
-				if _, ok := initialStatuses[iID]; ok {
-					t.Errorf("invalid test, duplicate initial status with key: %s source: %s", iID.key, iID.source.String())
-				}
-				initialStatuses[iID] = status
-			}
-
-			expectedStatuses := make(map[configID]*Status)
-			for _, status := range tc.expectedStatuses {
-				iID := configID{
-					source: status.Source,
-					key:    status.Key,
-				}
-				if _, ok := expectedStatuses[iID]; ok {
-					t.Errorf("invalid test, duplicate expected status with key: %s source: %s", iID.key, iID.source.String())
-				}
-				expectedStatuses[iID] = status
+			initialStatuses, err := buildStatuses(tc.initialStatuses)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
+
+			expectedStatuses, err := buildStatuses(tc.expectedStatuses)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
 			}
 			}
 
 
+			tempDir := os.TempDir()
+			path := filepath.Join(tempDir, configFile)
+			defer os.Remove(path)
+
 			// Initialize controller
 			// Initialize controller
 			icd := &Controller{
 			icd := &Controller{
-				statuses: initialStatuses,
+				path:     path,
 				watchers: tc.configWatchers,
 				watchers: tc.configWatchers,
 			}
 			}
+			err = icd.save(initialStatuses)
+			if err != nil {
+				t.Errorf("failed to save initial statuses: %s", err.Error())
+			}
+
+			// Functionality being tested
 			icd.pullWatchers()
 			icd.pullWatchers()
-			if len(icd.statuses) != len(tc.expectedStatuses) {
-				t.Errorf("integration statueses did not have the correct length actaul: %d, expected: %d", len(icd.statuses), len(tc.expectedStatuses))
+
+			// Test Result
+			status, err := icd.load()
+			if err != nil {
+				t.Errorf("failed to load status file: %s", err.Error())
+			}
+
+			err = checkStatuses(status, expectedStatuses)
+			if err != nil {
+				t.Errorf("statuses equality check failed: %s", err.Error())
+			}
+		})
+	}
+}
+
+func TestIntegrationController_CreateConfig(t *testing.T) {
+	testCases := map[string]struct {
+		initial   []*Status
+		expected  []*Status
+		input     cloudconfig.KeyedConfig
+		expectErr bool
+	}{
+		"Invalid Config": {
+			initial:   nil,
+			expected:  nil,
+			input:     invalidAthenaConf,
+			expectErr: true,
+		},
+		"config exists from this source": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			input:     validAthenaConf,
+			expectErr: true,
+		},
+		"config exists from this source altered": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			input:     validAthenaConfModifiedProperty,
+			expectErr: true,
+		},
+		"config exists from other source enabled": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, MultiCloudSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			input:     validAthenaConf,
+			expectErr: false,
+		},
+		"config exists from other source disabled": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			input:     validAthenaConf,
+			expectErr: false,
+		},
+		"config into empty": {
+			initial: []*Status{},
+			expected: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			input:     validAthenaConf,
+			expectErr: false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			// Test set up and validation
+			initialStatuses, err := buildStatuses(tc.initial)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
+
+			expectedStatuses, err := buildStatuses(tc.expected)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
+
+			tempDir := os.TempDir()
+			path := filepath.Join(tempDir, configFile)
+			defer os.Remove(path)
+
+			// Initialize controller
+			icd := &Controller{
+				path: path,
+			}
+			err = icd.save(initialStatuses)
+			if err != nil {
+				t.Errorf("failed to save initial statuses: %s", err.Error())
+			}
+
+			// Functionality being tested
+			err = icd.CreateConfig(tc.input)
+
+			// Test Result
+			if err != nil && !tc.expectErr {
+				t.Errorf("unexpected error when creating config: %s", err.Error())
+			}
+			if err == nil && tc.expectErr {
+				t.Errorf("no error where expect")
+			}
+
+			status, err := icd.load()
+			if err != nil {
+				t.Errorf("failed to load status file: %s", err.Error())
+			}
+
+			err = checkStatuses(status, expectedStatuses)
+			if err != nil {
+				t.Errorf("statuses equality check failed: %s", err.Error())
+			}
+		})
+	}
+
+}
+
+func TestIntegrationController_EnableConfig(t *testing.T) {
+	testCases := map[string]struct {
+		initial     []*Status
+		expected    []*Status
+		inputKey    string
+		inputSource string
+		expectErr   bool
+	}{
+		"config doesn't exist": {
+			initial:     []*Status{},
+			expected:    []*Status{},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   true,
+		},
+		"config is already enabled": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   true,
+		},
+		"alternate source": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, true, MultiCloudSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: MultiCloudSource.String(),
+			expectErr:   false,
+		},
+		"enabled disabled single config": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, false, ConfigControllerSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   false,
+		},
+		"enable config which is enabled by another source": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, false, ConfigControllerSource),
+				makeStatus(validAthenaConf, true, MultiCloudSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			// Test set up and validation
+			initialStatuses, err := buildStatuses(tc.initial)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
+
+			expectedStatuses, err := buildStatuses(tc.expected)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
+
+			tempDir := os.TempDir()
+			path := filepath.Join(tempDir, configFile)
+			defer os.Remove(path)
+
+			// Initialize controller
+			icd := &Controller{
+				path: path,
+			}
+			err = icd.save(initialStatuses)
+			if err != nil {
+				t.Errorf("failed to save initial statuses: %s", err.Error())
+			}
+
+			// Functionality being tested
+			err = icd.EnableConfig(tc.inputKey, tc.inputSource)
+
+			// Test Result
+			if err != nil && !tc.expectErr {
+				t.Errorf("unexpected error when enabling config: %s", err.Error())
+			}
+			if err == nil && tc.expectErr {
+				t.Errorf("no error where expect")
+			}
+
+			status, err := icd.load()
+			if err != nil {
+				t.Errorf("failed to load status file: %s", err.Error())
+			}
+
+			err = checkStatuses(status, expectedStatuses)
+			if err != nil {
+				t.Errorf("statuses equality check failed: %s", err.Error())
+			}
+		})
+	}
+}
+
+func TestIntegrationController_DisableConfig(t *testing.T) {
+	testCases := map[string]struct {
+		initial     []*Status
+		expected    []*Status
+		inputKey    string
+		inputSource string
+		expectErr   bool
+	}{
+		"config doesn't exist": {
+			initial:     []*Status{},
+			expected:    []*Status{},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   true,
+		},
+
+		"config is already disabled": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, false, ConfigControllerSource),
+				makeStatus(validAthenaConf, true, MultiCloudSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, false, ConfigControllerSource),
+				makeStatus(validAthenaConf, true, MultiCloudSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   true,
+		},
+		"disable single config": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, false, ConfigControllerSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   false,
+		},
+		"alternate source": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, MultiCloudSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: MultiCloudSource.String(),
+			expectErr:   false,
+		},
+		"disable config, matching config from separate source": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, false, ConfigControllerSource),
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   false,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			// Test set up and validation
+			initialStatuses, err := buildStatuses(tc.initial)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
+
+			expectedStatuses, err := buildStatuses(tc.expected)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
+
+			tempDir := os.TempDir()
+			path := filepath.Join(tempDir, configFile)
+			defer os.Remove(path)
+
+			// Initialize controller
+			icd := &Controller{
+				path: path,
+			}
+			err = icd.save(initialStatuses)
+			if err != nil {
+				t.Errorf("failed to save initial statuses: %s", err.Error())
+			}
+
+			// Functionality being tested
+			err = icd.DisableConfig(tc.inputKey, tc.inputSource)
+
+			// Test Result
+			if err != nil && !tc.expectErr {
+				t.Errorf("unexpected error when disabling config: %s", err.Error())
+			}
+			if err == nil && tc.expectErr {
+				t.Errorf("no error where expect")
+			}
+
+			status, err := icd.load()
+			if err != nil {
+				t.Errorf("failed to load status file: %s", err.Error())
+			}
+
+			err = checkStatuses(status, expectedStatuses)
+			if err != nil {
+				t.Errorf("statuses equality check failed: %s", err.Error())
 			}
 			}
+		})
+	}
+}
+
+func TestIntegrationController_DeleteConfig(t *testing.T) {
+	testCases := map[string]struct {
+		initial     []*Status
+		expected    []*Status
+		inputKey    string
+		inputSource string
+		expectErr   bool
+	}{
+		"config doesn't exist": {
+			initial:     []*Status{},
+			expected:    []*Status{},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   true,
+		},
+		"invalid source": {
+			initial:     []*Status{},
+			expected:    []*Status{},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: MultiCloudSource.String(),
+			expectErr:   true,
+		},
+		"delete single config": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+			},
+			expected:    []*Status{},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   false,
+		},
+		"disable config, matching config from separate source": {
+			initial: []*Status{
+				makeStatus(validAthenaConf, true, ConfigControllerSource),
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+			},
+			expected: []*Status{
+				makeStatus(validAthenaConf, false, MultiCloudSource),
+			},
+			inputKey:    validAthenaConf.Key(),
+			inputSource: ConfigControllerSource.String(),
+			expectErr:   false,
+		},
+	}
 
 
-			for iID, actualStatus := range icd.statuses {
-				expectedStatus, ok := expectedStatuses[iID]
-				if !ok {
-					t.Errorf("expected integration statuses is missing with integration ID: %v", iID)
-				}
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			// Test set up and validation
+			initialStatuses, err := buildStatuses(tc.initial)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
 
 
-				// failure here indicates an issue with the configID
-				if actualStatus.Key != expectedStatus.Key {
-					t.Errorf("integration status does not have the correct Key values actual: %s, expected: %s", actualStatus.Key, expectedStatus.Key)
-				}
+			expectedStatuses, err := buildStatuses(tc.expected)
+			if err != nil {
+				t.Errorf("initial statuses: %s", err.Error())
+			}
 
 
-				// failure here indicates an issue with the configID
-				if actualStatus.Key != expectedStatus.Key {
-					t.Errorf("integration status does not have the correct Source values actual: %s, expected: %s", actualStatus.Source, expectedStatus.Source)
-				}
+			tempDir := os.TempDir()
+			path := filepath.Join(tempDir, configFile)
+			defer os.Remove(path)
 
 
-				if actualStatus.Active != expectedStatus.Active {
-					t.Errorf("integration status does not have the correct Active values actual: %v, expected: %v", actualStatus.Active, expectedStatus.Active)
-				}
+			// Initialize controller
+			icd := &Controller{
+				path: path,
+			}
+			err = icd.save(initialStatuses)
+			if err != nil {
+				t.Errorf("failed to save initial statuses: %s", err.Error())
+			}
 
 
-				if actualStatus.Valid != expectedStatus.Valid {
-					t.Errorf("integration status does not have the correct Valid values actual: %v, expected: %v", actualStatus.Valid, expectedStatus.Valid)
-				}
+			// Functionality being tested
+			err = icd.DeleteConfig(tc.inputKey, tc.inputSource)
 
 
-				if !actualStatus.Config.Equals(expectedStatus.Config) {
-					t.Errorf("integration status does not have the correct config values actual: %v, expected: %v", actualStatus.Config, expectedStatus.Config)
-				}
+			// Test Result
+			if err != nil && !tc.expectErr {
+				t.Errorf("unexpected error when deleting config: %s", err.Error())
+			}
+			if err == nil && tc.expectErr {
+				t.Errorf("no error where expect")
+			}
+
+			status, err := icd.load()
+			if err != nil {
+				t.Errorf("failed to load status file: %s", err.Error())
+			}
+
+			err = checkStatuses(status, expectedStatuses)
+			if err != nil {
+				t.Errorf("statuses equality check failed: %s", err.Error())
 			}
 			}
 		})
 		})
 	}
 	}
 }
 }
+
+func makeStatus(config cloudconfig.KeyedConfig, active bool, source ConfigSource) *Status {
+	err := config.Validate()
+	valid := err == nil
+
+	configType, err := ConfigTypeFromConfig(config)
+	if err != nil {
+		panic(fmt.Errorf("config type not recognised: %w", err))
+	}
+
+	return &Status{
+		Source:     source,
+		Key:        config.Key(),
+		Active:     active,
+		Valid:      valid,
+		ConfigType: configType,
+		Config:     config,
+	}
+}
+
+func buildStatuses(statusList []*Status) (Statuses, error) {
+	statuses := Statuses{}
+	for _, status := range statusList {
+		if _, ok := statuses.Get(status.Key, status.Source); ok {
+			return nil, fmt.Errorf("invalid test, duplicate status with key: %s source: %s", status.Key, status.Source.String())
+		}
+		statuses.Insert(status)
+	}
+	return statuses, nil
+}
+
+func checkStatuses(actual, expected Statuses) error {
+	if len(actual.List()) != len(expected.List()) {
+		return fmt.Errorf("integration statueses did not have the correct length actaul: %d, expected: %d", len(actual.List()), len(expected.List()))
+	}
+
+	for _, actualStatus := range actual.List() {
+		expectedStatus, ok := expected.Get(actualStatus.Key, actualStatus.Source)
+		if !ok {
+			return fmt.Errorf("expected integration statuses is missing with integration key: %s, source: %s", actualStatus.Key, actualStatus.Source.String())
+		}
+
+		// failure here indicates an issue with the configID
+		if actualStatus.Key != expectedStatus.Key {
+			return fmt.Errorf("integration status does not have the correct Key values actual: %s, expected: %s", actualStatus.Key, expectedStatus.Key)
+		}
+
+		// failure here indicates an issue with the configID
+		if actualStatus.Key != expectedStatus.Key {
+			return fmt.Errorf("integration status does not have the correct Source values actual: %s, expected: %s", actualStatus.Source, expectedStatus.Source)
+		}
+
+		if actualStatus.Active != expectedStatus.Active {
+			return fmt.Errorf("integration status does not have the correct Active values actual: %v, expected: %v", actualStatus.Active, expectedStatus.Active)
+		}
+
+		if actualStatus.Valid != expectedStatus.Valid {
+			return fmt.Errorf("integration status does not have the correct Valid values actual: %v, expected: %v", actualStatus.Valid, expectedStatus.Valid)
+		}
+
+		if !actualStatus.Config.Equals(expectedStatus.Config) {
+			return fmt.Errorf("integration status does not have the correct config values actual: %v, expected: %v", actualStatus.Config, expectedStatus.Config)
+		}
+	}
+	return nil
+}

+ 145 - 0
pkg/cloud/config/statuses.go

@@ -0,0 +1,145 @@
+package config
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/opencost/opencost/core/pkg/util/json"
+	"github.com/opencost/opencost/pkg/cloud"
+	"github.com/opencost/opencost/pkg/cloud/aws"
+	"github.com/opencost/opencost/pkg/cloud/azure"
+	"github.com/opencost/opencost/pkg/cloud/gcp"
+)
+
+const (
+	S3ConfigType           = "s3"
+	AthenaConfigType       = "athena"
+	BigQueryConfigType     = "bigquery"
+	AzureStorageConfigType = "azurestorage"
+)
+
+func ConfigTypeFromConfig(config cloud.KeyedConfig) (string, error) {
+	switch config.(type) {
+	case *aws.S3Configuration:
+		return S3ConfigType, nil
+	case *aws.AthenaConfiguration:
+		return AthenaConfigType, nil
+	case *gcp.BigQueryConfiguration:
+		return BigQueryConfigType, nil
+	case *azure.StorageConfiguration:
+		return AzureStorageConfigType, nil
+	}
+	return "", fmt.Errorf("failed to config type for config with key: %s, type %T", config.Key(), config)
+}
+
+type Statuses map[ConfigSource]map[string]*Status
+
+func (s Statuses) Get(key string, source ConfigSource) (*Status, bool) {
+	if _, ok := s[source]; !ok {
+		return nil, false
+	}
+	status, ok := s[source][key]
+	return status, ok
+}
+
+func (s Statuses) Insert(status *Status) {
+	if _, ok := s[status.Source]; !ok {
+		s[status.Source] = map[string]*Status{}
+	}
+	s[status.Source][status.Key] = status
+}
+
+func (s Statuses) List() []*Status {
+	var list []*Status
+	for _, statusesByKey := range s {
+		for _, status := range statusesByKey {
+			list = append(list, status)
+		}
+	}
+	return list
+}
+
+type Status struct {
+	Source     ConfigSource      `json:"source"`
+	Key        string            `json:"key"`
+	Active     bool              `json:"active"`
+	Valid      bool              `json:"valid"`
+	ConfigType string            `json:"configType"`
+	Config     cloud.KeyedConfig `json:"config"`
+}
+
+func (s *Status) UnmarshalJSON(b []byte) error {
+	var f interface{}
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	fmap := f.(map[string]interface{})
+
+	sourceFloat, err := cloud.GetInterfaceValue[float64](fmap, "source")
+	if err != nil {
+		return fmt.Errorf("Status: UnmarshalJSON: %s", err.Error())
+	}
+	source := ConfigSource(int(sourceFloat))
+
+	key, err := cloud.GetInterfaceValue[string](fmap, "key")
+	if err != nil {
+		return fmt.Errorf("Status: UnmarshalJSON: %s", err.Error())
+	}
+
+	active, err := cloud.GetInterfaceValue[bool](fmap, "active")
+	if err != nil {
+		return fmt.Errorf("Status: UnmarshalJSON: %s", err.Error())
+	}
+
+	valid, err := cloud.GetInterfaceValue[bool](fmap, "valid")
+	if err != nil {
+		return fmt.Errorf("Status: UnmarshalJSON: %s", err.Error())
+	}
+
+	configType, err := cloud.GetInterfaceValue[string](fmap, "configType")
+	if err != nil {
+		return fmt.Errorf("Status: UnmarshalJSON: %s", err.Error())
+	}
+
+	// Pick correct implementation to unmarshal into
+	var config cloud.KeyedConfig
+	switch strings.ToLower(configType) {
+	case S3ConfigType:
+		config = &aws.S3Configuration{}
+	case AthenaConfigType:
+		config = &aws.AthenaConfiguration{}
+	case BigQueryConfigType:
+		config = &gcp.BigQueryConfiguration{}
+	case AzureStorageConfigType:
+		config = &azure.StorageConfiguration{}
+	default:
+		return fmt.Errorf("Status: UnmarshalJSON: config type '%s' is not recognized", configType)
+	}
+
+	configAny, err := cloud.GetInterfaceValue[any](fmap, "config")
+	if err != nil {
+		return fmt.Errorf("Status: UnmarshalJSON: %s", err.Error())
+	}
+
+	// convert the interface back to a []Byte so that it can be unmarshalled into the correct type
+	fBin, err := json.Marshal(configAny)
+	if err != nil {
+		return fmt.Errorf("Status: UnmarshalJSON: could not marshal value %v: %w", f, err)
+	}
+
+	err = json.Unmarshal(fBin, config)
+	if err != nil {
+		return fmt.Errorf("Status: UnmarshalJSON: failed to unmarshal into Configuration type %T from value %v: %w", config, f, err)
+	}
+
+	// Set Values
+	s.Source = source
+	s.Key = key
+	s.Active = active
+	s.Valid = valid
+	s.ConfigType = configType
+	s.Config = config
+	return nil
+}

+ 6 - 1
pkg/costmodel/cluster_helpers.go

@@ -640,7 +640,12 @@ func buildLabelsMap(
 		// ingested label data. This removes the label_ prefix that prometheus
 		// ingested label data. This removes the label_ prefix that prometheus
 		// adds to emitted labels. It also keeps from ingesting prometheus labels
 		// adds to emitted labels. It also keeps from ingesting prometheus labels
 		// that aren't a part of the asset.
 		// that aren't a part of the asset.
-		m[key] = result.GetLabels()
+		if _, ok := m[key]; !ok {
+			m[key] = map[string]string{}
+		}
+		for k, l := range result.GetLabels() {
+			m[key][k] = l
+		}
 	}
 	}
 	return m
 	return m
 }
 }

+ 8 - 5
ui/src/Reports.js

@@ -49,11 +49,14 @@ const aggregationOptions = [
   { name: "Cluster", value: "cluster" },
   { name: "Cluster", value: "cluster" },
   { name: "Node", value: "node" },
   { name: "Node", value: "node" },
   { name: "Namespace", value: "namespace" },
   { name: "Namespace", value: "namespace" },
-  { name: "Controller kind", value: "controllerKind" },
+  { name: "Controller Kind", value: "controllerKind" },
   { name: "Controller", value: "controller" },
   { name: "Controller", value: "controller" },
+  { name: "DaemonSet", value: "daemonset" },
+  { name: "Deployment", value: "deployment" },
+  { name: "Job", value: "job" },
   { name: "Service", value: "service" },
   { name: "Service", value: "service" },
+  { name: "StatefulSet", value: "statefulset" },
   { name: "Pod", value: "pod" },
   { name: "Pod", value: "pod" },
-  { name: "Deployment", value: "deployment" },
   { name: "Container", value: "container" },
   { name: "Container", value: "container" },
 ];
 ];
 
 
@@ -155,7 +158,7 @@ const ReportsPage = () => {
   const searchParams = new URLSearchParams(routerLocation.search);
   const searchParams = new URLSearchParams(routerLocation.search);
   const routerHistory = useHistory();
   const routerHistory = useHistory();
   useEffect(() => {
   useEffect(() => {
-    setWindow(searchParams.get("window") || "6d");
+    setWindow(searchParams.get("window") || "7d");
     setAggregateBy(searchParams.get("agg") || "namespace");
     setAggregateBy(searchParams.get("agg") || "namespace");
     setAccumulate(searchParams.get("acc") === "true" || false);
     setAccumulate(searchParams.get("acc") === "true" || false);
     setCurrency(searchParams.get("currency") || "USD");
     setCurrency(searchParams.get("currency") || "USD");
@@ -204,12 +207,12 @@ const ReportsPage = () => {
           {
           {
             primary: "Failed to load report data",
             primary: "Failed to load report data",
             secondary:
             secondary:
-              "Please update Kubecost to the latest version, then contact support if problems persist.",
+              "Please update OpenCost to the latest version, then open an Issue on GitHub if problems persist.",
           },
           },
         ]);
         ]);
       } else {
       } else {
         let secondary =
         let secondary =
-          "Please contact Kubecost support with a bug report if problems persist.";
+          "Please open an Issue on GitHub if problems persist.";
         if (err.message.length > 0) {
         if (err.message.length > 0) {
           secondary = err.message;
           secondary = err.message;
         }
         }

+ 2 - 2
ui/src/cloudCost/cloudCostDetails.js

@@ -73,12 +73,12 @@ const CloudCostDetails = ({
           {
           {
             primary: "Failed to load report data",
             primary: "Failed to load report data",
             secondary:
             secondary:
-              "Please update Kubecost to the latest version, then contact support if problems persist.",
+              "Please update OpenCost to the latest version, then open an Issue on GitHub if problems persist.",
           },
           },
         ]);
         ]);
       } else {
       } else {
         let secondary =
         let secondary =
-          "Please contact Kubecost support with a bug report if problems persist.";
+          "Please open an Issue on GitHub if problems persist.";
         if (err.message.length > 0) {
         if (err.message.length > 0) {
           secondary = err.message;
           secondary = err.message;
         }
         }

+ 1 - 0
ui/src/services/allocation.js

@@ -12,6 +12,7 @@ class AllocationService {
     const params = {
     const params = {
       window: win,
       window: win,
       aggregate: aggregate,
       aggregate: aggregate,
+      includeIdle: true,
       step: "1d",
       step: "1d",
     };
     };
     if (typeof accumulate === "boolean") {
     if (typeof accumulate === "boolean") {