mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-01-15 17:21:46 +01:00
Update dependencies. Rebuild graphql and swagger
This commit is contained in:
1178
api/swagger.json
1178
api/swagger.json
File diff suppressed because it is too large
Load Diff
690
api/swagger.yaml
690
api/swagger.yaml
File diff suppressed because it is too large
Load Diff
4
go.mod
4
go.mod
@@ -11,7 +11,7 @@ tool (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.85
|
github.com/99designs/gqlgen v0.17.85
|
||||||
github.com/ClusterCockpit/cc-lib/v2 v2.0.0
|
github.com/ClusterCockpit/cc-lib/v2 v2.1.0
|
||||||
github.com/Masterminds/squirrel v1.5.4
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.1
|
github.com/aws/aws-sdk-go-v2 v1.41.1
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.6
|
github.com/aws/aws-sdk-go-v2/config v1.32.6
|
||||||
@@ -109,7 +109,6 @@ require (
|
|||||||
github.com/urfave/cli/v2 v2.27.7 // indirect
|
github.com/urfave/cli/v2 v2.27.7 // indirect
|
||||||
github.com/urfave/cli/v3 v3.6.1 // indirect
|
github.com/urfave/cli/v3 v3.6.1 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||||
github.com/xtgo/set v1.0.0 // indirect
|
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
@@ -119,7 +118,6 @@ require (
|
|||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.40.0 // indirect
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
35
go.sum
35
go.sum
@@ -2,12 +2,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/99designs/gqlgen v0.17.85 h1:EkGx3U2FDcxQm8YDLQSpXIAVmpDyZ3IcBMOJi2nH1S0=
|
github.com/99designs/gqlgen v0.17.85 h1:EkGx3U2FDcxQm8YDLQSpXIAVmpDyZ3IcBMOJi2nH1S0=
|
||||||
github.com/99designs/gqlgen v0.17.85/go.mod h1:yvs8s0bkQlRfqg03YXr3eR4OQUowVhODT/tHzCXnbOU=
|
github.com/99designs/gqlgen v0.17.85/go.mod h1:yvs8s0bkQlRfqg03YXr3eR4OQUowVhODT/tHzCXnbOU=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/ClusterCockpit/cc-lib/v2 v2.0.0 h1:OjDADx8mf9SflqeeKUuhy5pamu4YDucae6wUX6vvNNA=
|
github.com/ClusterCockpit/cc-lib/v2 v2.1.0 h1:B6l6h0IjfEuY9DU6aVM3fSsj24lQ1eudXK9QTKmJjqg=
|
||||||
github.com/ClusterCockpit/cc-lib/v2 v2.0.0/go.mod h1:JuxMAuEOaLLNEnnL9U3ejha8kMvsSatLdKPZEgJw6iw=
|
github.com/ClusterCockpit/cc-lib/v2 v2.1.0/go.mod h1:JuxMAuEOaLLNEnnL9U3ejha8kMvsSatLdKPZEgJw6iw=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||||
@@ -74,10 +72,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
|||||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
@@ -89,16 +83,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||||
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
|
||||||
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
|
||||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
|
||||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
|
||||||
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||||
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
@@ -236,17 +220,8 @@ github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
|
||||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
|
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
|
||||||
@@ -318,8 +293,6 @@ github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkW
|
|||||||
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
||||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
github.com/xtgo/set v1.0.0 h1:6BCNBRv3ORNDQ7fyoJXRv+tstJz3m1JVFQErfeZz2pY=
|
|
||||||
github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ type GetClustersAPIResponse struct {
|
|||||||
// @description Get a list of all cluster configs. Specific cluster can be requested using query parameter.
|
// @description Get a list of all cluster configs. Specific cluster can be requested using query parameter.
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param cluster query string false "Job Cluster"
|
// @param cluster query string false "Job Cluster"
|
||||||
// @success 200 {object} api.GetClustersApiResponse "Array of clusters"
|
// @success 200 {object} api.GetClustersAPIResponse "Array of clusters"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
|
|||||||
1178
internal/api/docs.go
1178
internal/api/docs.go
File diff suppressed because it is too large
Load Diff
@@ -104,7 +104,7 @@ type JobMetricWithName struct {
|
|||||||
// @param items-per-page query int false "Items per page (Default: 25)"
|
// @param items-per-page query int false "Items per page (Default: 25)"
|
||||||
// @param page query int false "Page Number (Default: 1)"
|
// @param page query int false "Page Number (Default: 1)"
|
||||||
// @param with-metadata query bool false "Include metadata (e.g. jobScript) in response"
|
// @param with-metadata query bool false "Include metadata (e.g. jobScript) in response"
|
||||||
// @success 200 {object} api.GetJobsApiResponse "Job array and page info"
|
// @success 200 {object} api.GetJobsAPIResponse "Job array and page info"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
@@ -232,7 +232,7 @@ func (api *RestAPI) getJobs(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Database ID of Job"
|
// @param id path int true "Database ID of Job"
|
||||||
// @param all-metrics query bool false "Include all available metrics"
|
// @param all-metrics query bool false "Include all available metrics"
|
||||||
// @success 200 {object} api.GetJobApiResponse "Job resource"
|
// @success 200 {object} api.GetJobAPIResponse "Job resource"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
@@ -324,8 +324,8 @@ func (api *RestAPI) getCompleteJobByID(rw http.ResponseWriter, r *http.Request)
|
|||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Database ID of Job"
|
// @param id path int true "Database ID of Job"
|
||||||
// @param request body api.GetJobApiRequest true "Array of metric names"
|
// @param request body api.GetJobAPIRequest true "Array of metric names"
|
||||||
// @success 200 {object} api.GetJobApiResponse "Job resource"
|
// @success 200 {object} api.GetJobAPIResponse "Job resource"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
@@ -478,7 +478,7 @@ func (api *RestAPI) editMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Job Database ID"
|
// @param id path int true "Job Database ID"
|
||||||
// @param request body api.TagJobApiRequest true "Array of tag-objects to add"
|
// @param request body api.TagJobAPIRequest true "Array of tag-objects to add"
|
||||||
// @success 200 {object} schema.Job "Updated job resource"
|
// @success 200 {object} schema.Job "Updated job resource"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
@@ -542,7 +542,7 @@ func (api *RestAPI) tagJob(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Job Database ID"
|
// @param id path int true "Job Database ID"
|
||||||
// @param request body api.TagJobApiRequest true "Array of tag-objects to remove"
|
// @param request body api.TagJobAPIRequest true "Array of tag-objects to remove"
|
||||||
// @success 200 {object} schema.Job "Updated job resource"
|
// @success 200 {object} schema.Job "Updated job resource"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
@@ -606,7 +606,7 @@ func (api *RestAPI) removeTagJob(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @description Tag wills be removed from respective archive files.
|
// @description Tag wills be removed from respective archive files.
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce plain
|
// @produce plain
|
||||||
// @param request body api.TagJobApiRequest true "Array of tag-objects to remove"
|
// @param request body api.TagJobAPIRequest true "Array of tag-objects to remove"
|
||||||
// @success 200 {string} string "Success Response"
|
// @success 200 {string} string "Success Response"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
@@ -650,7 +650,7 @@ func (api *RestAPI) removeTags(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param request body schema.Job true "Job to add"
|
// @param request body schema.Job true "Job to add"
|
||||||
// @success 201 {object} api.DefaultApiResponse "Job added successfully"
|
// @success 201 {object} api.DefaultAPIResponse "Job added successfully"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
@@ -728,7 +728,7 @@ func (api *RestAPI) startJob(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @description Job to stop is specified by request body. All fields are required in this case.
|
// @description Job to stop is specified by request body. All fields are required in this case.
|
||||||
// @description Returns full job resource information according to 'Job' scheme.
|
// @description Returns full job resource information according to 'Job' scheme.
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param request body api.StopJobApiRequest true "All fields required"
|
// @param request body api.StopJobAPIRequest true "All fields required"
|
||||||
// @success 200 {object} schema.Job "Success message"
|
// @success 200 {object} schema.Job "Success message"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
@@ -754,7 +754,6 @@ func (api *RestAPI) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// cclog.Printf("loading db job for stopJobByRequest... : stopJobApiRequest=%v", req)
|
|
||||||
job, err = api.JobRepository.Find(req.JobID, req.Cluster, req.StartTime)
|
job, err = api.JobRepository.Find(req.JobID, req.Cluster, req.StartTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Try cached jobs if not found in main repository
|
// Try cached jobs if not found in main repository
|
||||||
@@ -776,7 +775,7 @@ func (api *RestAPI) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @description Job to remove is specified by database ID. This will not remove the job from the job archive.
|
// @description Job to remove is specified by database ID. This will not remove the job from the job archive.
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param id path int true "Database ID of Job"
|
// @param id path int true "Database ID of Job"
|
||||||
// @success 200 {object} api.DefaultApiResponse "Success message"
|
// @success 200 {object} api.DefaultAPIResponse "Success message"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
@@ -820,8 +819,8 @@ func (api *RestAPI) deleteJobByID(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @description Job to delete is specified by request body. All fields are required in this case.
|
// @description Job to delete is specified by request body. All fields are required in this case.
|
||||||
// @accept json
|
// @accept json
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param request body api.DeleteJobApiRequest true "All fields required"
|
// @param request body api.DeleteJobAPIRequest true "All fields required"
|
||||||
// @success 200 {object} api.DefaultApiResponse "Success message"
|
// @success 200 {object} api.DefaultAPIResponse "Success message"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
@@ -873,7 +872,7 @@ func (api *RestAPI) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
|
|||||||
// @description Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.
|
// @description Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param ts path int true "Unix epoch timestamp"
|
// @param ts path int true "Unix epoch timestamp"
|
||||||
// @success 200 {object} api.DefaultApiResponse "Success message"
|
// @success 200 {object} api.DefaultAPIResponse "Success message"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func determineState(states []string) schema.SchedulerState {
|
|||||||
// @description Required query-parameter defines if all users or only users with additional special roles are returned.
|
// @description Required query-parameter defines if all users or only users with additional special roles are returned.
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param request body UpdateNodeStatesRequest true "Request body containing nodes and their states"
|
// @param request body UpdateNodeStatesRequest true "Request body containing nodes and their states"
|
||||||
// @success 200 {object} api.DefaultApiResponse "Success message"
|
// @success 200 {object} api.DefaultAPIResponse "Success message"
|
||||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type APIReturnedUser struct {
|
|||||||
// @description Required query-parameter defines if all users or only users with additional special roles are returned.
|
// @description Required query-parameter defines if all users or only users with additional special roles are returned.
|
||||||
// @produce json
|
// @produce json
|
||||||
// @param not-just-user query bool true "If returned list should contain all users or only users with additional special roles"
|
// @param not-just-user query bool true "If returned list should contain all users or only users with additional special roles"
|
||||||
// @success 200 {array} api.ApiReturnedUser "List of users returned successfully"
|
// @success 200 {array} api.APIReturnedUser "List of users returned successfully"
|
||||||
// @failure 400 {string} string "Bad Request"
|
// @failure 400 {string} string "Bad Request"
|
||||||
// @failure 401 {string} string "Unauthorized"
|
// @failure 401 {string} string "Unauthorized"
|
||||||
// @failure 403 {string} string "Forbidden"
|
// @failure 403 {string} string "Forbidden"
|
||||||
|
|||||||
@@ -10815,7 +10815,7 @@ func (ec *executionContext) _SubCluster_metricConfig(ctx context.Context, field
|
|||||||
return obj.MetricConfig, nil
|
return obj.MetricConfig, nil
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
ec.marshalNMetricConfig2ᚕgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐMetricConfigᚄ,
|
ec.marshalNMetricConfig2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐMetricConfigᚄ,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
@@ -18466,11 +18466,7 @@ func (ec *executionContext) marshalNJobsStatistics2ᚖgithubᚗcomᚋClusterCock
|
|||||||
return ec._JobsStatistics(ctx, sel, v)
|
return ec._JobsStatistics(ctx, sel, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalNMetricConfig2githubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐMetricConfig(ctx context.Context, sel ast.SelectionSet, v schema.MetricConfig) graphql.Marshaler {
|
func (ec *executionContext) marshalNMetricConfig2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐMetricConfigᚄ(ctx context.Context, sel ast.SelectionSet, v []*schema.MetricConfig) graphql.Marshaler {
|
||||||
return ec._MetricConfig(ctx, sel, &v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalNMetricConfig2ᚕgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐMetricConfigᚄ(ctx context.Context, sel ast.SelectionSet, v []schema.MetricConfig) graphql.Marshaler {
|
|
||||||
ret := make(graphql.Array, len(v))
|
ret := make(graphql.Array, len(v))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
isLen1 := len(v) == 1
|
isLen1 := len(v) == 1
|
||||||
@@ -18494,7 +18490,7 @@ func (ec *executionContext) marshalNMetricConfig2ᚕgithubᚗcomᚋClusterCockpi
|
|||||||
if !isLen1 {
|
if !isLen1 {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
}
|
}
|
||||||
ret[i] = ec.marshalNMetricConfig2githubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐMetricConfig(ctx, sel, v[i])
|
ret[i] = ec.marshalNMetricConfig2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐMetricConfig(ctx, sel, v[i])
|
||||||
}
|
}
|
||||||
if isLen1 {
|
if isLen1 {
|
||||||
f(i)
|
f(i)
|
||||||
@@ -18514,6 +18510,16 @@ func (ec *executionContext) marshalNMetricConfig2ᚕgithubᚗcomᚋClusterCockpi
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNMetricConfig2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋv2ᚋschemaᚐMetricConfig(ctx context.Context, sel ast.SelectionSet, v *schema.MetricConfig) graphql.Marshaler {
|
||||||
|
if v == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||||
|
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
return ec._MetricConfig(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalNMetricFootprints2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐMetricFootprintsᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.MetricFootprints) graphql.Marshaler {
|
func (ec *executionContext) marshalNMetricFootprints2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐMetricFootprintsᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.MetricFootprints) graphql.Marshaler {
|
||||||
ret := make(graphql.Array, len(v))
|
ret := make(graphql.Array, len(v))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package graph
|
|||||||
// This file will be automatically regenerated based on the schema, any resolver
|
// This file will be automatically regenerated based on the schema, any resolver
|
||||||
// implementations
|
// implementations
|
||||||
// will be copied through when generating and any unknown code will be moved to the end.
|
// will be copied through when generating and any unknown code will be moved to the end.
|
||||||
// Code generated by github.com/99designs/gqlgen version v0.17.84
|
// Code generated by github.com/99designs/gqlgen version v0.17.85
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -283,7 +283,7 @@ func (r *mutationResolver) RemoveTagFromList(ctx context.Context, tagIds []strin
|
|||||||
// Test Access: Admins && Admin Tag OR Everyone && Private Tag
|
// Test Access: Admins && Admin Tag OR Everyone && Private Tag
|
||||||
if user.HasRole(schema.RoleAdmin) && (tscope == "global" || tscope == "admin") || user.Username == tscope {
|
if user.HasRole(schema.RoleAdmin) && (tscope == "global" || tscope == "admin") || user.Username == tscope {
|
||||||
// Remove from DB
|
// Remove from DB
|
||||||
if err = r.Repo.RemoveTagById(tid); err != nil {
|
if err = r.Repo.RemoveTagByID(tid); err != nil {
|
||||||
cclog.Warn("Error while removing tag")
|
cclog.Warn("Error while removing tag")
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package importer
|
package importer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package importer
|
package importer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func Init(rawConfig json.RawMessage, wg *sync.WaitGroup) {
|
|||||||
cclog.Debugf("[METRICSTORE]> Using %d workers for checkpoint/archive operations\n", Keys.NumWorkers)
|
cclog.Debugf("[METRICSTORE]> Using %d workers for checkpoint/archive operations\n", Keys.NumWorkers)
|
||||||
|
|
||||||
// Helper function to add metric configuration
|
// Helper function to add metric configuration
|
||||||
addMetricConfig := func(mc schema.MetricConfig) {
|
addMetricConfig := func(mc *schema.MetricConfig) {
|
||||||
agg, err := AssignAggregationStrategy(mc.Aggregation)
|
agg, err := AssignAggregationStrategy(mc.Aggregation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warnf("Could not find aggregation strategy for metric config '%s': %s", mc.Name, err.Error())
|
cclog.Warnf("Could not find aggregation strategy for metric config '%s': %s", mc.Name, err.Error())
|
||||||
@@ -88,7 +88,7 @@ func Init(rawConfig json.RawMessage, wg *sync.WaitGroup) {
|
|||||||
|
|
||||||
for _, c := range archive.Clusters {
|
for _, c := range archive.Clusters {
|
||||||
for _, mc := range c.MetricConfig {
|
for _, mc := range c.MetricConfig {
|
||||||
addMetricConfig(*mc)
|
addMetricConfig(mc)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sc := range c.SubClusters {
|
for _, sc := range c.SubClusters {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -686,7 +686,6 @@ func (r *JobRepository) AllocatedNodes(cluster string) (map[string]map[string]in
|
|||||||
return subclusters, nil
|
return subclusters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Set duration to requested walltime?
|
|
||||||
// StopJobsExceedingWalltimeBy marks running jobs as failed if they exceed their walltime limit.
|
// StopJobsExceedingWalltimeBy marks running jobs as failed if they exceed their walltime limit.
|
||||||
// This is typically called periodically to clean up stuck or orphaned jobs.
|
// This is typically called periodically to clean up stuck or orphaned jobs.
|
||||||
//
|
//
|
||||||
@@ -762,7 +761,6 @@ func (r *JobRepository) FindJobIdsByTag(tagID int64) ([]int64, error) {
|
|||||||
return jobIds, nil
|
return jobIds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Reconsider filtering short jobs with harcoded threshold
|
|
||||||
// FindRunningJobs returns all currently running jobs for a specific cluster.
|
// FindRunningJobs returns all currently running jobs for a specific cluster.
|
||||||
// Filters out short-running jobs based on repoConfig.MinRunningJobDuration threshold.
|
// Filters out short-running jobs based on repoConfig.MinRunningJobDuration threshold.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -90,13 +90,13 @@ func TestFindJobsBetween(t *testing.T) {
|
|||||||
|
|
||||||
// 2. Create a tag
|
// 2. Create a tag
|
||||||
tagName := fmt.Sprintf("testtag_%d", time.Now().UnixNano())
|
tagName := fmt.Sprintf("testtag_%d", time.Now().UnixNano())
|
||||||
tagId, err := r.CreateTag("testtype", tagName, "global")
|
tagID, err := r.CreateTag("testtype", tagName, "global")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Link Tag (Manually to avoid archive dependency side-effects in unit test)
|
// 3. Link Tag (Manually to avoid archive dependency side-effects in unit test)
|
||||||
_, err = r.DB.Exec("INSERT INTO jobtag (job_id, tag_id) VALUES (?, ?)", *targetJob.ID, tagId)
|
_, err = r.DB.Exec("INSERT INTO jobtag (job_id, tag_id) VALUES (?, ?)", *targetJob.ID, tagID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ func (r *NodeRepository) GetNodesForList(
|
|||||||
queryFilters = append(queryFilters, &model.NodeFilter{Hostname: &model.StringInput{Contains: &nodeFilter}})
|
queryFilters = append(queryFilters, &model.NodeFilter{Hostname: &model.StringInput{Contains: &nodeFilter}})
|
||||||
}
|
}
|
||||||
if stateFilter != "all" && stateFilter != "notindb" {
|
if stateFilter != "all" && stateFilter != "notindb" {
|
||||||
var queryState schema.SchedulerState = schema.SchedulerState(stateFilter)
|
queryState := schema.SchedulerState(stateFilter)
|
||||||
queryFilters = append(queryFilters, &model.NodeFilter{SchedulerState: &queryState})
|
queryFilters = append(queryFilters, &model.NodeFilter{SchedulerState: &queryState})
|
||||||
}
|
}
|
||||||
// if healthFilter != "all" {
|
// if healthFilter != "all" {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func BenchmarkSelect1(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkDB_FindJobById(b *testing.B) {
|
func BenchmarkDB_FindJobById(b *testing.B) {
|
||||||
var jobId int64 = 1677322
|
var jobID int64 = 1677322
|
||||||
|
|
||||||
b.Run("FindJobById", func(b *testing.B) {
|
b.Run("FindJobById", func(b *testing.B) {
|
||||||
db := setup(b)
|
db := setup(b)
|
||||||
@@ -55,7 +55,7 @@ func BenchmarkDB_FindJobById(b *testing.B) {
|
|||||||
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
_, err := db.FindByID(getContext(b), jobId)
|
_, err := db.FindByID(getContext(b), jobID)
|
||||||
noErr(b, err)
|
noErr(b, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -63,7 +63,7 @@ func BenchmarkDB_FindJobById(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkDB_FindJob(b *testing.B) {
|
func BenchmarkDB_FindJob(b *testing.B) {
|
||||||
var jobId int64 = 107266
|
var jobID int64 = 107266
|
||||||
var startTime int64 = 1657557241
|
var startTime int64 = 1657557241
|
||||||
cluster := "fritz"
|
cluster := "fritz"
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ func BenchmarkDB_FindJob(b *testing.B) {
|
|||||||
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
_, err := db.Find(&jobId, &cluster, &startTime)
|
_, err := db.Find(&jobID, &cluster, &startTime)
|
||||||
noErr(b, err)
|
noErr(b, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,44 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// This file contains job statistics and histogram generation functionality for the JobRepository.
|
||||||
|
//
|
||||||
|
// # Job Statistics
|
||||||
|
//
|
||||||
|
// The statistics methods provide aggregated metrics about jobs including total jobs, users,
|
||||||
|
// walltime, and resource usage (nodes, cores, accelerators). Statistics can be computed:
|
||||||
|
// - Overall (JobsStats): Single aggregate across all matching jobs
|
||||||
|
// - Grouped (JobsStatsGrouped): Aggregated by user, project, cluster, or subcluster
|
||||||
|
// - Counts (JobCountGrouped, AddJobCount): Simple job counts with optional filtering
|
||||||
|
//
|
||||||
|
// All statistics methods support filtering via JobFilter and respect security contexts.
|
||||||
|
//
|
||||||
|
// # Histograms
|
||||||
|
//
|
||||||
|
// Histogram methods generate distribution data for visualization:
|
||||||
|
// - Duration, nodes, cores, accelerators (AddHistograms)
|
||||||
|
// - Job metrics like CPU load, memory usage (AddMetricHistograms)
|
||||||
|
//
|
||||||
|
// Histograms use intelligent binning:
|
||||||
|
// - Duration: Variable bin sizes (1m, 10m, 1h, 6h, 12h, 24h) with zero-padding
|
||||||
|
// - Resources: Natural value-based bins
|
||||||
|
// - Metrics: Normalized to peak values with configurable bin counts
|
||||||
|
//
|
||||||
|
// # Running vs. Completed Jobs
|
||||||
|
//
|
||||||
|
// Statistics handle running jobs specially:
|
||||||
|
// - Duration calculated as (now - start_time) for running jobs
|
||||||
|
// - Metric histograms for running jobs load data from metric backend instead of footprint
|
||||||
|
// - Job state filtering distinguishes running/completed jobs
|
||||||
|
//
|
||||||
|
// # Performance Considerations
|
||||||
|
//
|
||||||
|
// - All queries use prepared statements via stmtCache
|
||||||
|
// - Complex aggregations use SQL for efficiency
|
||||||
|
// - Histogram pre-initialization ensures consistent bin ranges
|
||||||
|
// - Metric histogram queries limited to 500 jobs for running job analysis
|
||||||
|
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -19,7 +57,9 @@ import (
|
|||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GraphQL validation should make sure that no unkown values can be specified.
|
// groupBy2column maps GraphQL Aggregate enum values to their corresponding database column names.
|
||||||
|
// Used by JobsStatsGrouped and JobCountGrouped to translate user-facing grouping dimensions
|
||||||
|
// into SQL GROUP BY clauses. GraphQL validation ensures only valid enum values are accepted.
|
||||||
var groupBy2column = map[model.Aggregate]string{
|
var groupBy2column = map[model.Aggregate]string{
|
||||||
model.AggregateUser: "job.hpc_user",
|
model.AggregateUser: "job.hpc_user",
|
||||||
model.AggregateProject: "job.project",
|
model.AggregateProject: "job.project",
|
||||||
@@ -27,6 +67,9 @@ var groupBy2column = map[model.Aggregate]string{
|
|||||||
model.AggregateSubcluster: "job.subcluster",
|
model.AggregateSubcluster: "job.subcluster",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sortBy2column maps GraphQL SortByAggregate enum values to their corresponding computed column names.
|
||||||
|
// Used by JobsStatsGrouped to translate sort preferences into SQL ORDER BY clauses.
|
||||||
|
// Column names match the AS aliases used in buildStatsQuery.
|
||||||
var sortBy2column = map[model.SortByAggregate]string{
|
var sortBy2column = map[model.SortByAggregate]string{
|
||||||
model.SortByAggregateTotaljobs: "totalJobs",
|
model.SortByAggregateTotaljobs: "totalJobs",
|
||||||
model.SortByAggregateTotalusers: "totalUsers",
|
model.SortByAggregateTotalusers: "totalUsers",
|
||||||
@@ -39,6 +82,21 @@ var sortBy2column = map[model.SortByAggregate]string{
|
|||||||
model.SortByAggregateTotalacchours: "totalAccHours",
|
model.SortByAggregateTotalacchours: "totalAccHours",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildCountQuery constructs a SQL query to count jobs with optional grouping and filtering.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filter: Job filters to apply (cluster, user, time range, etc.)
|
||||||
|
// - kind: Special filter - "running" for running jobs only, "short" for jobs under threshold
|
||||||
|
// - col: Column name to GROUP BY; empty string for total count without grouping
|
||||||
|
//
|
||||||
|
// Returns a SelectBuilder that produces either:
|
||||||
|
// - Single count: COUNT(job.id) when col is empty
|
||||||
|
// - Grouped counts: col, COUNT(job.id) when col is specified
|
||||||
|
//
|
||||||
|
// The kind parameter enables counting specific job categories:
|
||||||
|
// - "running": Only jobs with job_state = 'running'
|
||||||
|
// - "short": Only jobs with duration < ShortRunningJobsDuration config value
|
||||||
|
// - empty: All jobs matching filters
|
||||||
func (r *JobRepository) buildCountQuery(
|
func (r *JobRepository) buildCountQuery(
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
kind string,
|
kind string,
|
||||||
@@ -47,10 +105,8 @@ func (r *JobRepository) buildCountQuery(
|
|||||||
var query sq.SelectBuilder
|
var query sq.SelectBuilder
|
||||||
|
|
||||||
if col != "" {
|
if col != "" {
|
||||||
// Scan columns: id, cnt
|
|
||||||
query = sq.Select(col, "COUNT(job.id)").From("job").GroupBy(col)
|
query = sq.Select(col, "COUNT(job.id)").From("job").GroupBy(col)
|
||||||
} else {
|
} else {
|
||||||
// Scan columns: cnt
|
|
||||||
query = sq.Select("COUNT(job.id)").From("job")
|
query = sq.Select("COUNT(job.id)").From("job")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +124,27 @@ func (r *JobRepository) buildCountQuery(
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildStatsQuery constructs a SQL query to compute comprehensive job statistics with optional grouping.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filter: Job filters to apply (cluster, user, time range, etc.)
|
||||||
|
// - col: Column name to GROUP BY; empty string for overall statistics without grouping
|
||||||
|
//
|
||||||
|
// Returns a SelectBuilder that produces comprehensive statistics:
|
||||||
|
// - totalJobs: Count of jobs
|
||||||
|
// - totalUsers: Count of distinct users (always 0 when grouping by user)
|
||||||
|
// - totalWalltime: Sum of job durations in hours
|
||||||
|
// - totalNodes: Sum of nodes used across all jobs
|
||||||
|
// - totalNodeHours: Sum of (duration × num_nodes) in hours
|
||||||
|
// - totalCores: Sum of hardware threads used across all jobs
|
||||||
|
// - totalCoreHours: Sum of (duration × num_hwthreads) in hours
|
||||||
|
// - totalAccs: Sum of accelerators used across all jobs
|
||||||
|
// - totalAccHours: Sum of (duration × num_acc) in hours
|
||||||
|
//
|
||||||
|
// Special handling:
|
||||||
|
// - Running jobs: Duration calculated as (now - start_time) instead of stored duration
|
||||||
|
// - Grouped queries: Also select grouping column and user's display name from hpc_user table
|
||||||
|
// - All time values converted from seconds to hours (÷ 3600) and rounded
|
||||||
func (r *JobRepository) buildStatsQuery(
|
func (r *JobRepository) buildStatsQuery(
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
col string,
|
col string,
|
||||||
@@ -75,31 +152,29 @@ func (r *JobRepository) buildStatsQuery(
|
|||||||
var query sq.SelectBuilder
|
var query sq.SelectBuilder
|
||||||
|
|
||||||
if col != "" {
|
if col != "" {
|
||||||
// Scan columns: id, name, totalJobs, totalUsers, totalWalltime, totalNodes, totalNodeHours, totalCores, totalCoreHours, totalAccs, totalAccHours
|
|
||||||
query = sq.Select(
|
query = sq.Select(
|
||||||
col,
|
col,
|
||||||
"name",
|
"name",
|
||||||
"COUNT(job.id) as totalJobs",
|
"COUNT(job.id) as totalJobs",
|
||||||
"COUNT(DISTINCT job.hpc_user) AS totalUsers",
|
"COUNT(DISTINCT job.hpc_user) AS totalUsers",
|
||||||
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END)) / 3600) as int) as totalWalltime`, time.Now().Unix()),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END)) / 3600) as int) as totalWalltime`, time.Now().Unix()),
|
||||||
fmt.Sprintf(`CAST(SUM(job.num_nodes) as int) as totalNodes`),
|
`CAST(SUM(job.num_nodes) as int) as totalNodes`,
|
||||||
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_nodes) / 3600) as int) as totalNodeHours`, time.Now().Unix()),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_nodes) / 3600) as int) as totalNodeHours`, time.Now().Unix()),
|
||||||
fmt.Sprintf(`CAST(SUM(job.num_hwthreads) as int) as totalCores`),
|
`CAST(SUM(job.num_hwthreads) as int) as totalCores`,
|
||||||
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_hwthreads) / 3600) as int) as totalCoreHours`, time.Now().Unix()),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_hwthreads) / 3600) as int) as totalCoreHours`, time.Now().Unix()),
|
||||||
fmt.Sprintf(`CAST(SUM(job.num_acc) as int) as totalAccs`),
|
`CAST(SUM(job.num_acc) as int) as totalAccs`,
|
||||||
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_acc) / 3600) as int) as totalAccHours`, time.Now().Unix()),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_acc) / 3600) as int) as totalAccHours`, time.Now().Unix()),
|
||||||
).From("job").LeftJoin("hpc_user ON hpc_user.username = job.hpc_user").GroupBy(col)
|
).From("job").LeftJoin("hpc_user ON hpc_user.username = job.hpc_user").GroupBy(col)
|
||||||
} else {
|
} else {
|
||||||
// Scan columns: totalJobs, totalUsers, totalWalltime, totalNodes, totalNodeHours, totalCores, totalCoreHours, totalAccs, totalAccHours
|
|
||||||
query = sq.Select(
|
query = sq.Select(
|
||||||
"COUNT(job.id) as totalJobs",
|
"COUNT(job.id) as totalJobs",
|
||||||
"COUNT(DISTINCT job.hpc_user) AS totalUsers",
|
"COUNT(DISTINCT job.hpc_user) AS totalUsers",
|
||||||
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END)) / 3600) as int)`, time.Now().Unix()),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END)) / 3600) as int)`, time.Now().Unix()),
|
||||||
fmt.Sprintf(`CAST(SUM(job.num_nodes) as int)`),
|
`CAST(SUM(job.num_nodes) as int)`,
|
||||||
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_nodes) / 3600) as int)`, time.Now().Unix()),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_nodes) / 3600) as int)`, time.Now().Unix()),
|
||||||
fmt.Sprintf(`CAST(SUM(job.num_hwthreads) as int)`),
|
`CAST(SUM(job.num_hwthreads) as int)`,
|
||||||
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_hwthreads) / 3600) as int)`, time.Now().Unix()),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_hwthreads) / 3600) as int)`, time.Now().Unix()),
|
||||||
fmt.Sprintf(`CAST(SUM(job.num_acc) as int)`),
|
`CAST(SUM(job.num_acc) as int)`,
|
||||||
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_acc) / 3600) as int)`, time.Now().Unix()),
|
fmt.Sprintf(`CAST(ROUND(SUM((CASE WHEN job.job_state = "running" THEN %d - job.start_time ELSE job.duration END) * job.num_acc) / 3600) as int)`, time.Now().Unix()),
|
||||||
).From("job")
|
).From("job")
|
||||||
}
|
}
|
||||||
@@ -111,6 +186,25 @@ func (r *JobRepository) buildStatsQuery(
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobsStatsGrouped computes comprehensive job statistics grouped by a dimension (user, project, cluster, or subcluster).
|
||||||
|
//
|
||||||
|
// This is the primary method for generating aggregated statistics views in the UI, providing
|
||||||
|
// metrics like total jobs, walltime, and resource usage broken down by the specified grouping.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for security checks and cancellation
|
||||||
|
// - filter: Filters to apply (time range, cluster, job state, etc.)
|
||||||
|
// - page: Optional pagination (ItemsPerPage: -1 disables pagination)
|
||||||
|
// - sortBy: Optional sort column (totalJobs, totalWalltime, totalCoreHours, etc.)
|
||||||
|
// - groupBy: Required grouping dimension (User, Project, Cluster, or Subcluster)
|
||||||
|
//
|
||||||
|
// Returns a slice of JobsStatistics, one per group, with:
|
||||||
|
// - ID: The group identifier (username, project name, cluster name, etc.)
|
||||||
|
// - Name: Display name (for users, from hpc_user.name; empty for other groups)
|
||||||
|
// - Statistics: totalJobs, totalUsers, totalWalltime, resource usage metrics
|
||||||
|
//
|
||||||
|
// Security: Respects user roles via SecurityCheck - users see only their own data unless admin/support.
|
||||||
|
// Performance: Results are sorted in SQL and pagination applied before scanning rows.
|
||||||
func (r *JobRepository) JobsStatsGrouped(
|
func (r *JobRepository) JobsStatsGrouped(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
@@ -230,6 +324,21 @@ func (r *JobRepository) JobsStatsGrouped(
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobsStats computes overall job statistics across all matching jobs without grouping.
|
||||||
|
//
|
||||||
|
// This method provides a single aggregate view of job metrics, useful for dashboard
|
||||||
|
// summaries and overall system utilization reports.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for security checks and cancellation
|
||||||
|
// - filter: Filters to apply (time range, cluster, job state, etc.)
|
||||||
|
//
|
||||||
|
// Returns a single-element slice containing aggregate statistics:
|
||||||
|
// - totalJobs, totalUsers, totalWalltime
|
||||||
|
// - totalNodeHours, totalCoreHours, totalAccHours
|
||||||
|
//
|
||||||
|
// Unlike JobsStatsGrouped, this returns overall totals without breaking down by dimension.
|
||||||
|
// Security checks are applied via SecurityCheck to respect user access levels.
|
||||||
func (r *JobRepository) JobsStats(
|
func (r *JobRepository) JobsStats(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
@@ -303,6 +412,17 @@ func LoadJobStat(job *schema.Job, metric string, statType string) float64 {
|
|||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobCountGrouped counts jobs grouped by a dimension without computing detailed statistics.
|
||||||
|
//
|
||||||
|
// This is a lightweight alternative to JobsStatsGrouped when only job counts are needed,
|
||||||
|
// avoiding the overhead of calculating walltime and resource usage metrics.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for security checks
|
||||||
|
// - filter: Filters to apply
|
||||||
|
// - groupBy: Grouping dimension (User, Project, Cluster, or Subcluster)
|
||||||
|
//
|
||||||
|
// Returns JobsStatistics with only ID and TotalJobs populated for each group.
|
||||||
func (r *JobRepository) JobCountGrouped(
|
func (r *JobRepository) JobCountGrouped(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
@@ -343,6 +463,20 @@ func (r *JobRepository) JobCountGrouped(
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddJobCountGrouped augments existing statistics with additional job counts by category.
|
||||||
|
//
|
||||||
|
// This method enriches JobsStatistics returned by JobsStatsGrouped or JobCountGrouped
|
||||||
|
// with counts of running or short-running jobs, matched by group ID.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for security checks
|
||||||
|
// - filter: Filters to apply
|
||||||
|
// - groupBy: Grouping dimension (must match the dimension used for stats parameter)
|
||||||
|
// - stats: Existing statistics to augment (modified in-place by ID matching)
|
||||||
|
// - kind: "running" to add RunningJobs count, "short" to add ShortJobs count
|
||||||
|
//
|
||||||
|
// Returns the same stats slice with RunningJobs or ShortJobs fields populated per group.
|
||||||
|
// Groups without matching jobs will have 0 for the added field.
|
||||||
func (r *JobRepository) AddJobCountGrouped(
|
func (r *JobRepository) AddJobCountGrouped(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
@@ -392,6 +526,18 @@ func (r *JobRepository) AddJobCountGrouped(
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddJobCount augments existing overall statistics with additional job counts by category.
|
||||||
|
//
|
||||||
|
// Similar to AddJobCountGrouped but for ungrouped statistics. Applies the same count
|
||||||
|
// to all statistics entries (typically just one).
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for security checks
|
||||||
|
// - filter: Filters to apply
|
||||||
|
// - stats: Existing statistics to augment (modified in-place)
|
||||||
|
// - kind: "running" to add RunningJobs count, "short" to add ShortJobs count
|
||||||
|
//
|
||||||
|
// Returns the same stats slice with RunningJobs or ShortJobs fields set to the total count.
|
||||||
func (r *JobRepository) AddJobCount(
|
func (r *JobRepository) AddJobCount(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
@@ -437,6 +583,26 @@ func (r *JobRepository) AddJobCount(
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddHistograms augments statistics with distribution histograms for job properties.
|
||||||
|
//
|
||||||
|
// Generates histogram data for visualization of job duration, node count, core count,
|
||||||
|
// and accelerator count distributions. Duration histogram uses intelligent binning based
|
||||||
|
// on the requested resolution.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for security checks
|
||||||
|
// - filter: Filters to apply to jobs included in histograms
|
||||||
|
// - stat: Statistics struct to augment (modified in-place)
|
||||||
|
// - durationBins: Bin size - "1m", "10m", "1h", "6h", "12h", or "24h" (default)
|
||||||
|
//
|
||||||
|
// Populates these fields in stat:
|
||||||
|
// - HistDuration: Job duration distribution (zero-padded bins)
|
||||||
|
// - HistNumNodes: Node count distribution
|
||||||
|
// - HistNumCores: Core (hwthread) count distribution
|
||||||
|
// - HistNumAccs: Accelerator count distribution
|
||||||
|
//
|
||||||
|
// Duration bins are pre-initialized with zeros to ensure consistent ranges for visualization.
|
||||||
|
// Bin size determines both the width and maximum duration displayed (e.g., "1h" = 48 bins × 1h = 48h max).
|
||||||
func (r *JobRepository) AddHistograms(
|
func (r *JobRepository) AddHistograms(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
@@ -447,20 +613,20 @@ func (r *JobRepository) AddHistograms(
|
|||||||
|
|
||||||
var targetBinCount int
|
var targetBinCount int
|
||||||
var targetBinSize int
|
var targetBinSize int
|
||||||
switch {
|
switch *durationBins {
|
||||||
case *durationBins == "1m": // 1 Minute Bins + Max 60 Bins -> Max 60 Minutes
|
case "1m": // 1 Minute Bins + Max 60 Bins -> Max 60 Minutes
|
||||||
targetBinCount = 60
|
targetBinCount = 60
|
||||||
targetBinSize = 60
|
targetBinSize = 60
|
||||||
case *durationBins == "10m": // 10 Minute Bins + Max 72 Bins -> Max 12 Hours
|
case "10m": // 10 Minute Bins + Max 72 Bins -> Max 12 Hours
|
||||||
targetBinCount = 72
|
targetBinCount = 72
|
||||||
targetBinSize = 600
|
targetBinSize = 600
|
||||||
case *durationBins == "1h": // 1 Hour Bins + Max 48 Bins -> Max 48 Hours
|
case "1h": // 1 Hour Bins + Max 48 Bins -> Max 48 Hours
|
||||||
targetBinCount = 48
|
targetBinCount = 48
|
||||||
targetBinSize = 3600
|
targetBinSize = 3600
|
||||||
case *durationBins == "6h": // 6 Hour Bins + Max 12 Bins -> Max 3 Days
|
case "6h": // 6 Hour Bins + Max 12 Bins -> Max 3 Days
|
||||||
targetBinCount = 12
|
targetBinCount = 12
|
||||||
targetBinSize = 21600
|
targetBinSize = 21600
|
||||||
case *durationBins == "12h": // 12 hour Bins + Max 14 Bins -> Max 7 Days
|
case "12h": // 12 hour Bins + Max 14 Bins -> Max 7 Days
|
||||||
targetBinCount = 14
|
targetBinCount = 14
|
||||||
targetBinSize = 43200
|
targetBinSize = 43200
|
||||||
default: // 24h
|
default: // 24h
|
||||||
@@ -499,7 +665,30 @@ func (r *JobRepository) AddHistograms(
|
|||||||
return stat, nil
|
return stat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requires thresholds for metric from config for cluster? Of all clusters and use largest? split to 10 + 1 for artifacts?
|
// AddMetricHistograms augments statistics with distribution histograms for job metrics.
|
||||||
|
//
|
||||||
|
// Generates histogram data for metrics like CPU load, memory usage, etc. Handles running
|
||||||
|
// and completed jobs differently: running jobs load data from metric backend, completed jobs
|
||||||
|
// use footprint data from database.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ctx: Context for security checks
|
||||||
|
// - filter: Filters to apply (MUST contain State filter for running jobs)
|
||||||
|
// - metrics: List of metric names to histogram (e.g., ["cpu_load", "mem_used"])
|
||||||
|
// - stat: Statistics struct to augment (modified in-place)
|
||||||
|
// - targetBinCount: Number of histogram bins (default: 10)
|
||||||
|
//
|
||||||
|
// Populates HistMetrics field in stat with MetricHistoPoints for each metric.
|
||||||
|
//
|
||||||
|
// Binning algorithm:
|
||||||
|
// - Values normalized to metric's peak value from cluster configuration
|
||||||
|
// - Bins evenly distributed from 0 to peak
|
||||||
|
// - Pre-initialized with zeros for consistent visualization
|
||||||
|
//
|
||||||
|
// Limitations:
|
||||||
|
// - Running jobs: Limited to 500 jobs for performance
|
||||||
|
// - Requires valid cluster configuration with metric peak values
|
||||||
|
// - Uses footprint statistic (avg/max/min) configured per metric
|
||||||
func (r *JobRepository) AddMetricHistograms(
|
func (r *JobRepository) AddMetricHistograms(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
filter []*model.JobFilter,
|
filter []*model.JobFilter,
|
||||||
@@ -534,7 +723,16 @@ func (r *JobRepository) AddMetricHistograms(
|
|||||||
return stat, nil
|
return stat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// `value` must be the column grouped by, but renamed to "value"
|
// jobsStatisticsHistogram generates a simple histogram by grouping on a column value.
|
||||||
|
//
|
||||||
|
// Used for histograms where the column value directly represents the bin (e.g., node count, core count).
|
||||||
|
// Unlike duration/metric histograms, this doesn't pre-initialize bins with zeros.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - value: SQL expression that produces the histogram value, aliased as "value"
|
||||||
|
// - filters: Job filters to apply
|
||||||
|
//
|
||||||
|
// Returns histogram points with Value (from column) and Count (number of jobs).
|
||||||
func (r *JobRepository) jobsStatisticsHistogram(
|
func (r *JobRepository) jobsStatisticsHistogram(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
value string,
|
value string,
|
||||||
@@ -573,6 +771,26 @@ func (r *JobRepository) jobsStatisticsHistogram(
|
|||||||
return points, nil
|
return points, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jobsDurationStatisticsHistogram generates a duration histogram with pre-initialized bins.
|
||||||
|
//
|
||||||
|
// Bins are zero-padded to provide consistent ranges for visualization, unlike simple
|
||||||
|
// histograms which only return bins with data. The value parameter should compute
|
||||||
|
// the bin number from job duration.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - value: SQL expression computing bin number from duration, aliased as "value"
|
||||||
|
// - filters: Job filters to apply
|
||||||
|
// - binSizeSeconds: Width of each bin in seconds
|
||||||
|
// - targetBinCount: Number of bins to pre-initialize
|
||||||
|
//
|
||||||
|
// Returns histogram points with Value (bin_number × binSizeSeconds) and Count.
|
||||||
|
// All bins from 1 to targetBinCount are returned, with Count=0 for empty bins.
|
||||||
|
//
|
||||||
|
// Algorithm:
|
||||||
|
// 1. Pre-initialize targetBinCount bins with zero counts
|
||||||
|
// 2. Query database for actual counts per bin
|
||||||
|
// 3. Match query results to pre-initialized bins by value
|
||||||
|
// 4. Bins without matches remain at zero
|
||||||
func (r *JobRepository) jobsDurationStatisticsHistogram(
|
func (r *JobRepository) jobsDurationStatisticsHistogram(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
value string,
|
value string,
|
||||||
@@ -588,7 +806,6 @@ func (r *JobRepository) jobsDurationStatisticsHistogram(
|
|||||||
return nil, qerr
|
return nil, qerr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize histogram bins with zero counts
|
|
||||||
// Each bin represents a duration range: bin N = [N*binSizeSeconds, (N+1)*binSizeSeconds)
|
// Each bin represents a duration range: bin N = [N*binSizeSeconds, (N+1)*binSizeSeconds)
|
||||||
// Example: binSizeSeconds=3600 (1 hour), bin 1 = 0-1h, bin 2 = 1-2h, etc.
|
// Example: binSizeSeconds=3600 (1 hour), bin 1 = 0-1h, bin 2 = 1-2h, etc.
|
||||||
points := make([]*model.HistoPoint, 0)
|
points := make([]*model.HistoPoint, 0)
|
||||||
@@ -607,8 +824,8 @@ func (r *JobRepository) jobsDurationStatisticsHistogram(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match query results to pre-initialized bins and fill counts
|
// Match query results to pre-initialized bins.
|
||||||
// Query returns raw duration values that need to be mapped to correct bins
|
// point.Value from query is the bin number; multiply by binSizeSeconds to match bin.Value.
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
point := model.HistoPoint{}
|
point := model.HistoPoint{}
|
||||||
if err := rows.Scan(&point.Value, &point.Count); err != nil {
|
if err := rows.Scan(&point.Value, &point.Count); err != nil {
|
||||||
@@ -616,13 +833,8 @@ func (r *JobRepository) jobsDurationStatisticsHistogram(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find matching bin and update count
|
|
||||||
// point.Value is multiplied by binSizeSeconds to match pre-calculated bin.Value
|
|
||||||
for _, e := range points {
|
for _, e := range points {
|
||||||
if e.Value == (point.Value * binSizeSeconds) {
|
if e.Value == (point.Value * binSizeSeconds) {
|
||||||
// Note: Matching on unmodified integer value (and multiplying point.Value
|
|
||||||
// by binSizeSeconds after match) causes frontend to loop into highest
|
|
||||||
// targetBinCount, due to zoom condition instantly being fulfilled (cause unknown)
|
|
||||||
e.Count = point.Count
|
e.Count = point.Count
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -633,13 +845,34 @@ func (r *JobRepository) jobsDurationStatisticsHistogram(
|
|||||||
return points, nil
|
return points, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jobsMetricStatisticsHistogram generates a metric histogram using footprint data from completed jobs.
|
||||||
|
//
|
||||||
|
// Values are normalized to the metric's peak value and distributed into bins. The algorithm
|
||||||
|
// is based on SQL histogram generation techniques, extracting metric values from JSON footprint
|
||||||
|
// and computing bin assignments in SQL.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - metric: Metric name (e.g., "cpu_load", "mem_used")
|
||||||
|
// - filters: Job filters to apply
|
||||||
|
// - bins: Number of bins to generate
|
||||||
|
//
|
||||||
|
// Returns MetricHistoPoints with metric name, unit, footprint stat type, and binned data.
|
||||||
|
//
|
||||||
|
// Algorithm:
|
||||||
|
// 1. Determine peak value from cluster configuration (filtered cluster or max across all)
|
||||||
|
// 2. Generate SQL that extracts footprint value, normalizes to [0,1], multiplies by bin count
|
||||||
|
// 3. Pre-initialize bins with min/max ranges based on peak value
|
||||||
|
// 4. Query database for counts per bin
|
||||||
|
// 5. Match results to pre-initialized bins
|
||||||
|
//
|
||||||
|
// Special handling: Values exactly equal to peak are forced into the last bin by multiplying
|
||||||
|
// peak by 0.999999999 to avoid creating an extra bin.
|
||||||
func (r *JobRepository) jobsMetricStatisticsHistogram(
|
func (r *JobRepository) jobsMetricStatisticsHistogram(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
metric string,
|
metric string,
|
||||||
filters []*model.JobFilter,
|
filters []*model.JobFilter,
|
||||||
bins *int,
|
bins *int,
|
||||||
) (*model.MetricHistoPoints, error) {
|
) (*model.MetricHistoPoints, error) {
|
||||||
// Determine the metric's peak value for histogram normalization
|
|
||||||
// Peak value defines the upper bound for binning: values are distributed across
|
// Peak value defines the upper bound for binning: values are distributed across
|
||||||
// bins from 0 to peak. First try to get peak from filtered cluster, otherwise
|
// bins from 0 to peak. First try to get peak from filtered cluster, otherwise
|
||||||
// scan all clusters to find the maximum peak value.
|
// scan all clusters to find the maximum peak value.
|
||||||
@@ -679,18 +912,14 @@ func (r *JobRepository) jobsMetricStatisticsHistogram(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct SQL histogram bins using normalized values
|
// Construct SQL histogram bins using normalized values.
|
||||||
// Algorithm based on: https://jereze.com/code/sql-histogram/ (modified)
|
// Algorithm based on: https://jereze.com/code/sql-histogram/ (modified)
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
// Calculate bin number for each job's metric value:
|
// Bin calculation formula:
|
||||||
// 1. Extract metric value from JSON footprint
|
// bin_number = CAST( (value / peak) * num_bins AS INTEGER ) + 1
|
||||||
// 2. Normalize to [0,1] by dividing by peak
|
// Special case: value == peak would create bin N+1, so we test for equality
|
||||||
// 3. Multiply by number of bins to get bin number
|
// and multiply peak by 0.999999999 to force it into bin N.
|
||||||
// 4. Cast to integer for bin assignment
|
|
||||||
//
|
|
||||||
// Special case: Values exactly equal to peak would fall into bin N+1,
|
|
||||||
// so we multiply peak by 0.999999999 to force it into the last bin (bin N)
|
|
||||||
binQuery := fmt.Sprintf(`CAST(
|
binQuery := fmt.Sprintf(`CAST(
|
||||||
((case when json_extract(footprint, "$.%s") = %f then %f*0.999999999 else json_extract(footprint, "$.%s") end) / %f)
|
((case when json_extract(footprint, "$.%s") = %f then %f*0.999999999 else json_extract(footprint, "$.%s") end) / %f)
|
||||||
* %v as INTEGER )`,
|
* %v as INTEGER )`,
|
||||||
@@ -699,24 +928,19 @@ func (r *JobRepository) jobsMetricStatisticsHistogram(
|
|||||||
mainQuery := sq.Select(
|
mainQuery := sq.Select(
|
||||||
fmt.Sprintf(`%s + 1 as bin`, binQuery),
|
fmt.Sprintf(`%s + 1 as bin`, binQuery),
|
||||||
`count(*) as count`,
|
`count(*) as count`,
|
||||||
// For Debug: // fmt.Sprintf(`CAST((%f / %d) as INTEGER ) * %s as min`, peak, *bins, binQuery),
|
|
||||||
// For Debug: // fmt.Sprintf(`CAST((%f / %d) as INTEGER ) * (%s + 1) as max`, peak, *bins, binQuery),
|
|
||||||
).From("job").Where(
|
).From("job").Where(
|
||||||
"JSON_VALID(footprint)",
|
"JSON_VALID(footprint)",
|
||||||
).Where(fmt.Sprintf(`json_extract(footprint, "$.%s") is not null and json_extract(footprint, "$.%s") <= %f`, (metric + "_" + footprintStat), (metric + "_" + footprintStat), peak))
|
).Where(fmt.Sprintf(`json_extract(footprint, "$.%s") is not null and json_extract(footprint, "$.%s") <= %f`, (metric + "_" + footprintStat), (metric + "_" + footprintStat), peak))
|
||||||
|
|
||||||
// Only accessible Jobs...
|
|
||||||
mainQuery, qerr := SecurityCheck(ctx, mainQuery)
|
mainQuery, qerr := SecurityCheck(ctx, mainQuery)
|
||||||
if qerr != nil {
|
if qerr != nil {
|
||||||
return nil, qerr
|
return nil, qerr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters...
|
|
||||||
for _, f := range filters {
|
for _, f := range filters {
|
||||||
mainQuery = BuildWhereClause(f, mainQuery)
|
mainQuery = BuildWhereClause(f, mainQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize query with Grouping and Ordering
|
|
||||||
mainQuery = mainQuery.GroupBy("bin").OrderBy("bin")
|
mainQuery = mainQuery.GroupBy("bin").OrderBy("bin")
|
||||||
|
|
||||||
rows, err := mainQuery.RunWith(r.DB).Query()
|
rows, err := mainQuery.RunWith(r.DB).Query()
|
||||||
@@ -725,8 +949,7 @@ func (r *JobRepository) jobsMetricStatisticsHistogram(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize histogram bins with calculated min/max ranges
|
// Pre-initialize bins with calculated min/max ranges.
|
||||||
// Each bin represents a range of metric values
|
|
||||||
// Example: peak=1000, bins=10 -> bin 1=[0,100), bin 2=[100,200), ..., bin 10=[900,1000]
|
// Example: peak=1000, bins=10 -> bin 1=[0,100), bin 2=[100,200), ..., bin 10=[900,1000]
|
||||||
points := make([]*model.MetricHistoPoint, 0)
|
points := make([]*model.MetricHistoPoint, 0)
|
||||||
binStep := int(peak) / *bins
|
binStep := int(peak) / *bins
|
||||||
@@ -737,29 +960,18 @@ func (r *JobRepository) jobsMetricStatisticsHistogram(
|
|||||||
points = append(points, &epoint)
|
points = append(points, &epoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill counts from query results
|
// Match query results to pre-initialized bins.
|
||||||
// Query only returns bins that have jobs, so we match against pre-initialized bins
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
rpoint := model.MetricHistoPoint{}
|
rpoint := model.MetricHistoPoint{}
|
||||||
if err := rows.Scan(&rpoint.Bin, &rpoint.Count); err != nil { // Required for Debug: &rpoint.Min, &rpoint.Max
|
if err := rows.Scan(&rpoint.Bin, &rpoint.Count); err != nil {
|
||||||
cclog.Warnf("Error while scanning rows for %s", metric)
|
cclog.Warnf("Error while scanning rows for %s", metric)
|
||||||
return nil, err // FIXME: Totally bricks cc-backend if returned and if all metrics requested?
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match query result to pre-initialized bin and update count
|
|
||||||
for _, e := range points {
|
for _, e := range points {
|
||||||
if e.Bin != nil && rpoint.Bin != nil {
|
if e.Bin != nil && rpoint.Bin != nil && *e.Bin == *rpoint.Bin {
|
||||||
if *e.Bin == *rpoint.Bin {
|
e.Count = rpoint.Count
|
||||||
e.Count = rpoint.Count
|
break
|
||||||
// Only Required For Debug: Check DB returned Min/Max against Backend Init above
|
|
||||||
// if rpoint.Min != nil {
|
|
||||||
// cclog.Warnf(">>>> Bin %d Min Set For %s to %d (Init'd with: %d)", *e.Bin, metric, *rpoint.Min, *e.Min)
|
|
||||||
// }
|
|
||||||
// if rpoint.Max != nil {
|
|
||||||
// cclog.Warnf(">>>> Bin %d Max Set For %s to %d (Init'd with: %d)", *e.Bin, metric, *rpoint.Max, *e.Max)
|
|
||||||
// }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -770,6 +982,28 @@ func (r *JobRepository) jobsMetricStatisticsHistogram(
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runningJobsMetricStatisticsHistogram generates metric histograms for running jobs using live data.
|
||||||
|
//
|
||||||
|
// Unlike completed jobs which use footprint data from the database, running jobs require
|
||||||
|
// fetching current metric averages from the metric backend (via metricdispatch).
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - metrics: List of metric names
|
||||||
|
// - filters: Job filters (should filter to running jobs only)
|
||||||
|
// - bins: Number of histogram bins
|
||||||
|
//
|
||||||
|
// Returns slice of MetricHistoPoints, one per metric.
|
||||||
|
//
|
||||||
|
// Limitations:
|
||||||
|
// - Maximum 500 jobs (returns nil if more jobs match)
|
||||||
|
// - Requires metric backend availability
|
||||||
|
// - Bins based on metric peak values from cluster configuration
|
||||||
|
//
|
||||||
|
// Algorithm:
|
||||||
|
// 1. Query first 501 jobs to check count limit
|
||||||
|
// 2. Load metric averages for all jobs via metricdispatch
|
||||||
|
// 3. For each metric, create bins based on peak value
|
||||||
|
// 4. Iterate averages and count jobs per bin
|
||||||
func (r *JobRepository) runningJobsMetricStatisticsHistogram(
|
func (r *JobRepository) runningJobsMetricStatisticsHistogram(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
metrics []string,
|
metrics []string,
|
||||||
|
|||||||
@@ -3,6 +3,34 @@
|
|||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package repository provides data access and persistence layer for ClusterCockpit.
|
||||||
|
//
|
||||||
|
// This file implements tag management functionality for job categorization and classification.
|
||||||
|
// Tags support both manual assignment (via REST/GraphQL APIs) and automatic detection
|
||||||
|
// (via tagger plugins). The implementation includes role-based access control through
|
||||||
|
// tag scopes and maintains bidirectional consistency between the SQL database and
|
||||||
|
// the file-based job archive.
|
||||||
|
//
|
||||||
|
// Database Schema:
|
||||||
|
//
|
||||||
|
// CREATE TABLE tag (
|
||||||
|
// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
// tag_type VARCHAR(255) NOT NULL,
|
||||||
|
// tag_name VARCHAR(255) NOT NULL,
|
||||||
|
// tag_scope VARCHAR(255) NOT NULL DEFAULT "global",
|
||||||
|
// CONSTRAINT tag_unique UNIQUE (tag_type, tag_name, tag_scope)
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// CREATE TABLE jobtag (
|
||||||
|
// job_id INTEGER,
|
||||||
|
// tag_id INTEGER,
|
||||||
|
// PRIMARY KEY (job_id, tag_id),
|
||||||
|
// FOREIGN KEY (job_id) REFERENCES job(id) ON DELETE CASCADE,
|
||||||
|
// FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// The jobtag junction table enables many-to-many relationships between jobs and tags.
|
||||||
|
// CASCADE deletion ensures referential integrity when jobs or tags are removed.
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -73,7 +101,7 @@ func (r *JobRepository) AddTagDirect(job int64, tag int64) ([]*schema.Tag, error
|
|||||||
func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema.Tag, error) {
|
func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema.Tag, error) {
|
||||||
j, err := r.FindByIDWithUser(user, job)
|
j, err := r.FindByIDWithUser(user, job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warn("Error while finding job by id")
|
cclog.Warnf("Error while finding job %d for user %s during tag removal: %v", job, user.Username, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +121,7 @@ func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema.
|
|||||||
|
|
||||||
archiveTags, err := r.getArchiveTags(&job)
|
archiveTags, err := r.getArchiveTags(&job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warn("Error while getting tags for job")
|
cclog.Warnf("Error while getting archive tags for job %d in RemoveTag: %v", job, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +132,7 @@ func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema.
|
|||||||
// Requires user authentication for security checks. Used by REST API.
|
// Requires user authentication for security checks. Used by REST API.
|
||||||
func (r *JobRepository) RemoveJobTagByRequest(user *schema.User, job int64, tagType string, tagName string, tagScope string) ([]*schema.Tag, error) {
|
func (r *JobRepository) RemoveJobTagByRequest(user *schema.User, job int64, tagType string, tagName string, tagScope string) ([]*schema.Tag, error) {
|
||||||
// Get Tag ID to delete
|
// Get Tag ID to delete
|
||||||
tagID, exists := r.TagId(tagType, tagName, tagScope)
|
tagID, exists := r.TagID(tagType, tagName, tagScope)
|
||||||
if !exists {
|
if !exists {
|
||||||
cclog.Warnf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
cclog.Warnf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
||||||
return nil, fmt.Errorf("tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
return nil, fmt.Errorf("tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
||||||
@@ -113,7 +141,7 @@ func (r *JobRepository) RemoveJobTagByRequest(user *schema.User, job int64, tagT
|
|||||||
// Get Job
|
// Get Job
|
||||||
j, err := r.FindByIDWithUser(user, job)
|
j, err := r.FindByIDWithUser(user, job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warn("Error while finding job by id")
|
cclog.Warnf("Error while finding job %d for user %s during tag removal by request: %v", job, user.Username, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,19 +156,30 @@ func (r *JobRepository) RemoveJobTagByRequest(user *schema.User, job int64, tagT
|
|||||||
|
|
||||||
tags, err := r.GetTags(user, &job)
|
tags, err := r.GetTags(user, &job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warn("Error while getting tags for job")
|
cclog.Warnf("Error while getting tags for job %d in RemoveJobTagByRequest: %v", job, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
archiveTags, err := r.getArchiveTags(&job)
|
archiveTags, err := r.getArchiveTags(&job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warn("Error while getting tags for job")
|
cclog.Warnf("Error while getting archive tags for job %d in RemoveJobTagByRequest: %v", job, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags, archive.UpdateTags(j, archiveTags)
|
return tags, archive.UpdateTags(j, archiveTags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeTagFromArchiveJobs updates the job archive for all affected jobs after a tag deletion.
|
||||||
|
//
|
||||||
|
// This function is called asynchronously (via goroutine) after removing a tag from the database
|
||||||
|
// to synchronize the file-based job archive with the database state. Errors are logged but not
|
||||||
|
// returned since this runs in the background.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - jobIds: Database IDs of all jobs that had the deleted tag
|
||||||
|
//
|
||||||
|
// Implementation note: Each job is processed individually to handle partial failures gracefully.
|
||||||
|
// If one job fails to update, others will still be processed.
|
||||||
func (r *JobRepository) removeTagFromArchiveJobs(jobIds []int64) {
|
func (r *JobRepository) removeTagFromArchiveJobs(jobIds []int64) {
|
||||||
for _, j := range jobIds {
|
for _, j := range jobIds {
|
||||||
tags, err := r.getArchiveTags(&j)
|
tags, err := r.getArchiveTags(&j)
|
||||||
@@ -163,18 +202,18 @@ func (r *JobRepository) removeTagFromArchiveJobs(jobIds []int64) {
|
|||||||
// Used by REST API. Does not update tagged jobs in Job archive.
|
// Used by REST API. Does not update tagged jobs in Job archive.
|
||||||
func (r *JobRepository) RemoveTagByRequest(tagType string, tagName string, tagScope string) error {
|
func (r *JobRepository) RemoveTagByRequest(tagType string, tagName string, tagScope string) error {
|
||||||
// Get Tag ID to delete
|
// Get Tag ID to delete
|
||||||
tagID, exists := r.TagId(tagType, tagName, tagScope)
|
tagID, exists := r.TagID(tagType, tagName, tagScope)
|
||||||
if !exists {
|
if !exists {
|
||||||
cclog.Warnf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
cclog.Warnf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
||||||
return fmt.Errorf("tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
return fmt.Errorf("tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.RemoveTagById(tagID)
|
return r.RemoveTagByID(tagID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes a tag from db by tag id
|
// Removes a tag from db by tag id
|
||||||
// Used by GraphQL API.
|
// Used by GraphQL API.
|
||||||
func (r *JobRepository) RemoveTagById(tagID int64) error {
|
func (r *JobRepository) RemoveTagByID(tagID int64) error {
|
||||||
jobIds, err := r.FindJobIdsByTag(tagID)
|
jobIds, err := r.FindJobIdsByTag(tagID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -213,7 +252,7 @@ func (r *JobRepository) RemoveTagById(tagID int64) error {
|
|||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// tagID, err := repo.CreateTag("performance", "high-memory", "global")
|
// tagID, err := repo.CreateTag("performance", "high-memory", "global")
|
||||||
func (r *JobRepository) CreateTag(tagType string, tagName string, tagScope string) (tagId int64, err error) {
|
func (r *JobRepository) CreateTag(tagType string, tagName string, tagScope string) (tagID int64, err error) {
|
||||||
// Default to "Global" scope if none defined
|
// Default to "Global" scope if none defined
|
||||||
if tagScope == "" {
|
if tagScope == "" {
|
||||||
tagScope = "global"
|
tagScope = "global"
|
||||||
@@ -300,13 +339,13 @@ func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var tagType string
|
var tagType string
|
||||||
var tagName string
|
var tagName string
|
||||||
var tagId int
|
var tagID int
|
||||||
var count int
|
var count int
|
||||||
if err = rows.Scan(&tagType, &tagName, &tagId, &count); err != nil {
|
if err = rows.Scan(&tagType, &tagName, &tagID, &count); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
// Use tagId as second Map-Key component to differentiate tags with identical names
|
// Use tagId as second Map-Key component to differentiate tags with identical names
|
||||||
counts[fmt.Sprint(tagType, tagName, tagId)] = count
|
counts[fmt.Sprint(tagType, tagName, tagID)] = count
|
||||||
}
|
}
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
|
|
||||||
@@ -314,18 +353,44 @@ func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTagNotFound = errors.New("the tag does not exist")
|
// ErrTagNotFound is returned when a tag ID or tag identifier (type, name, scope) does not exist in the database.
|
||||||
ErrJobNotOwned = errors.New("user is not owner of job")
|
ErrTagNotFound = errors.New("the tag does not exist")
|
||||||
ErrTagNoAccess = errors.New("user not permitted to use that tag")
|
|
||||||
ErrTagPrivateScope = errors.New("tag is private to another user")
|
// ErrJobNotOwned is returned when a user attempts to tag a job they do not have permission to access.
|
||||||
ErrTagAdminScope = errors.New("tag requires admin privileges")
|
ErrJobNotOwned = errors.New("user is not owner of job")
|
||||||
|
|
||||||
|
// ErrTagNoAccess is returned when a user attempts to use a tag they cannot access due to scope restrictions.
|
||||||
|
ErrTagNoAccess = errors.New("user not permitted to use that tag")
|
||||||
|
|
||||||
|
// ErrTagPrivateScope is returned when a user attempts to access another user's private tag.
|
||||||
|
ErrTagPrivateScope = errors.New("tag is private to another user")
|
||||||
|
|
||||||
|
// ErrTagAdminScope is returned when a non-admin user attempts to use an admin-scoped tag.
|
||||||
|
ErrTagAdminScope = errors.New("tag requires admin privileges")
|
||||||
|
|
||||||
|
// ErrTagsIncompatScopes is returned when attempting to combine admin and non-admin scoped tags in a single operation.
|
||||||
ErrTagsIncompatScopes = errors.New("combining admin and non-admin scoped tags not allowed")
|
ErrTagsIncompatScopes = errors.New("combining admin and non-admin scoped tags not allowed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// addJobTag is a helper function that inserts a job-tag association and updates the archive.
|
// addJobTag is a helper function that inserts a job-tag association and updates the archive.
|
||||||
// Returns the updated tag list for the job.
|
//
|
||||||
func (r *JobRepository) addJobTag(jobId int64, tagId int64, job *schema.Job, getTags func() ([]*schema.Tag, error)) ([]*schema.Tag, error) {
|
// This function performs three operations atomically:
|
||||||
q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(jobId, tagId)
|
// 1. Inserts the job-tag association into the jobtag junction table
|
||||||
|
// 2. Retrieves the updated tag list for the job (using the provided getTags callback)
|
||||||
|
// 3. Updates the job archive with the new tags to maintain database-archive consistency
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - jobId: Database ID of the job
|
||||||
|
// - tagId: Database ID of the tag to associate
|
||||||
|
// - job: Full job object needed for archive update
|
||||||
|
// - getTags: Callback function to retrieve updated tags (allows different security contexts)
|
||||||
|
//
|
||||||
|
// Returns the complete updated tag list for the job or an error.
|
||||||
|
//
|
||||||
|
// Note: This function does NOT validate tag scope permissions - callers must perform
|
||||||
|
// authorization checks before invoking this helper.
|
||||||
|
func (r *JobRepository) addJobTag(jobID int64, tagID int64, job *schema.Job, getTags func() ([]*schema.Tag, error)) ([]*schema.Tag, error) {
|
||||||
|
q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(jobID, tagID)
|
||||||
|
|
||||||
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
|
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
|
||||||
s, _, _ := q.ToSql()
|
s, _, _ := q.ToSql()
|
||||||
@@ -335,13 +400,13 @@ func (r *JobRepository) addJobTag(jobId int64, tagId int64, job *schema.Job, get
|
|||||||
|
|
||||||
tags, err := getTags()
|
tags, err := getTags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warnf("Error getting tags for job %d: %v", jobId, err)
|
cclog.Warnf("Error getting tags for job %d: %v", jobID, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
archiveTags, err := r.getArchiveTags(&jobId)
|
archiveTags, err := r.getArchiveTags(&jobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warnf("Error getting archive tags for job %d: %v", jobId, err)
|
cclog.Warnf("Error getting archive tags for job %d: %v", jobID, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +415,7 @@ func (r *JobRepository) addJobTag(jobId int64, tagId int64, job *schema.Job, get
|
|||||||
|
|
||||||
// AddTagOrCreate adds the tag with the specified type and name to the job with the database id `jobId`.
|
// AddTagOrCreate adds the tag with the specified type and name to the job with the database id `jobId`.
|
||||||
// If such a tag does not yet exist, it is created.
|
// If such a tag does not yet exist, it is created.
|
||||||
func (r *JobRepository) AddTagOrCreate(user *schema.User, jobId int64, tagType string, tagName string, tagScope string) (tagId int64, err error) {
|
func (r *JobRepository) AddTagOrCreate(user *schema.User, jobID int64, tagType string, tagName string, tagScope string) (tagID int64, err error) {
|
||||||
// Default to "Global" scope if none defined
|
// Default to "Global" scope if none defined
|
||||||
if tagScope == "" {
|
if tagScope == "" {
|
||||||
tagScope = "global"
|
tagScope = "global"
|
||||||
@@ -364,44 +429,43 @@ func (r *JobRepository) AddTagOrCreate(user *schema.User, jobId int64, tagType s
|
|||||||
return 0, fmt.Errorf("cannot write tag scope with current authorization")
|
return 0, fmt.Errorf("cannot write tag scope with current authorization")
|
||||||
}
|
}
|
||||||
|
|
||||||
tagId, exists := r.TagId(tagType, tagName, tagScope)
|
tagID, exists := r.TagID(tagType, tagName, tagScope)
|
||||||
if !exists {
|
if !exists {
|
||||||
tagId, err = r.CreateTag(tagType, tagName, tagScope)
|
tagID, err = r.CreateTag(tagType, tagName, tagScope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := r.AddTag(user, jobId, tagId); err != nil {
|
if _, err := r.AddTag(user, jobID, tagID); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return tagId, nil
|
return tagID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// used in auto tagger plugins
|
func (r *JobRepository) AddTagOrCreateDirect(jobID int64, tagType string, tagName string) (tagID int64, err error) {
|
||||||
func (r *JobRepository) AddTagOrCreateDirect(jobId int64, tagType string, tagName string) (tagId int64, err error) {
|
|
||||||
tagScope := "global"
|
tagScope := "global"
|
||||||
|
|
||||||
tagId, exists := r.TagId(tagType, tagName, tagScope)
|
tagID, exists := r.TagID(tagType, tagName, tagScope)
|
||||||
if !exists {
|
if !exists {
|
||||||
tagId, err = r.CreateTag(tagType, tagName, tagScope)
|
tagID, err = r.CreateTag(tagType, tagName, tagScope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := r.AddTagDirect(jobId, tagId); err != nil {
|
if _, err := r.AddTagDirect(jobID, tagID); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return tagId, nil
|
return tagID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *JobRepository) HasTag(jobId int64, tagType string, tagName string) bool {
|
func (r *JobRepository) HasTag(jobID int64, tagType string, tagName string) bool {
|
||||||
var id int64
|
var id int64
|
||||||
q := sq.Select("id").From("tag").Join("jobtag ON jobtag.tag_id = tag.id").
|
q := sq.Select("id").From("tag").Join("jobtag ON jobtag.tag_id = tag.id").
|
||||||
Where("jobtag.job_id = ?", jobId).Where("tag.tag_type = ?", tagType).
|
Where("jobtag.job_id = ?", jobID).Where("tag.tag_type = ?", tagType).
|
||||||
Where("tag.tag_name = ?", tagName)
|
Where("tag.tag_name = ?", tagName)
|
||||||
err := q.RunWith(r.stmtCache).QueryRow().Scan(&id)
|
err := q.RunWith(r.stmtCache).QueryRow().Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -411,21 +475,21 @@ func (r *JobRepository) HasTag(jobId int64, tagType string, tagName string) bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagId returns the database id of the tag with the specified type and name.
|
// TagID returns the database id of the tag with the specified type and name.
|
||||||
func (r *JobRepository) TagId(tagType string, tagName string, tagScope string) (tagId int64, exists bool) {
|
func (r *JobRepository) TagID(tagType string, tagName string, tagScope string) (tagID int64, exists bool) {
|
||||||
exists = true
|
exists = true
|
||||||
if err := sq.Select("id").From("tag").
|
if err := sq.Select("id").From("tag").
|
||||||
Where("tag.tag_type = ?", tagType).Where("tag.tag_name = ?", tagName).Where("tag.tag_scope = ?", tagScope).
|
Where("tag.tag_type = ?", tagType).Where("tag.tag_name = ?", tagName).Where("tag.tag_scope = ?", tagScope).
|
||||||
RunWith(r.stmtCache).QueryRow().Scan(&tagId); err != nil {
|
RunWith(r.stmtCache).QueryRow().Scan(&tagID); err != nil {
|
||||||
exists = false
|
exists = false
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagInfo returns the database infos of the tag with the specified id.
|
// TagInfo returns the database infos of the tag with the specified id.
|
||||||
func (r *JobRepository) TagInfo(tagId int64) (tagType string, tagName string, tagScope string, exists bool) {
|
func (r *JobRepository) TagInfo(tagID int64) (tagType string, tagName string, tagScope string, exists bool) {
|
||||||
exists = true
|
exists = true
|
||||||
if err := sq.Select("tag.tag_type", "tag.tag_name", "tag.tag_scope").From("tag").Where("tag.id = ?", tagId).
|
if err := sq.Select("tag.tag_type", "tag.tag_name", "tag.tag_scope").From("tag").Where("tag.id = ?", tagID).
|
||||||
RunWith(r.stmtCache).QueryRow().Scan(&tagType, &tagName, &tagScope); err != nil {
|
RunWith(r.stmtCache).QueryRow().Scan(&tagType, &tagName, &tagScope); err != nil {
|
||||||
exists = false
|
exists = false
|
||||||
}
|
}
|
||||||
@@ -450,7 +514,7 @@ func (r *JobRepository) GetTags(user *schema.User, job *int64) ([]*schema.Tag, e
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
tag := &schema.Tag{}
|
tag := &schema.Tag{}
|
||||||
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
|
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
|
||||||
cclog.Warn("Error while scanning rows")
|
cclog.Warnf("Error while scanning tag rows in GetTags: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Handle Scope Filtering: Tag Scope is Global, Private (== Username) or User is auth'd to view Admin Tags
|
// Handle Scope Filtering: Tag Scope is Global, Private (== Username) or User is auth'd to view Admin Tags
|
||||||
@@ -483,7 +547,7 @@ func (r *JobRepository) GetTagsDirect(job *int64) ([]*schema.Tag, error) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
tag := &schema.Tag{}
|
tag := &schema.Tag{}
|
||||||
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
|
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
|
||||||
cclog.Warn("Error while scanning rows")
|
cclog.Warnf("Error while scanning tag rows in GetTagsDirect: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
@@ -492,7 +556,18 @@ func (r *JobRepository) GetTagsDirect(job *int64) ([]*schema.Tag, error) {
|
|||||||
return tags, nil
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetArchiveTags returns a list of all tags *regardless of scope* for archiving if job is nil or of the tags that the job with that database ID has.
|
// getArchiveTags returns all tags for a job WITHOUT applying scope-based filtering.
|
||||||
|
//
|
||||||
|
// This internal function is used exclusively for job archive synchronization where we need
|
||||||
|
// to store all tags regardless of the current user's permissions. Unlike GetTags() which
|
||||||
|
// filters by scope, this returns the complete unfiltered tag list.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - job: Pointer to job database ID, or nil to return all tags in the system
|
||||||
|
//
|
||||||
|
// Returns all tags without scope filtering, used only for archive operations.
|
||||||
|
//
|
||||||
|
// WARNING: Do NOT expose this function to user-facing APIs as it bypasses authorization.
|
||||||
func (r *JobRepository) getArchiveTags(job *int64) ([]*schema.Tag, error) {
|
func (r *JobRepository) getArchiveTags(job *int64) ([]*schema.Tag, error) {
|
||||||
q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag")
|
q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag")
|
||||||
if job != nil {
|
if job != nil {
|
||||||
@@ -510,7 +585,7 @@ func (r *JobRepository) getArchiveTags(job *int64) ([]*schema.Tag, error) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
tag := &schema.Tag{}
|
tag := &schema.Tag{}
|
||||||
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
|
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
|
||||||
cclog.Warn("Error while scanning rows")
|
cclog.Warnf("Error while scanning tag rows in getArchiveTags: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
@@ -519,18 +594,18 @@ func (r *JobRepository) getArchiveTags(job *int64) ([]*schema.Tag, error) {
|
|||||||
return tags, nil
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *JobRepository) ImportTag(jobId int64, tagType string, tagName string, tagScope string) (err error) {
|
func (r *JobRepository) ImportTag(jobID int64, tagType string, tagName string, tagScope string) (err error) {
|
||||||
// Import has no scope ctx, only import from metafile to DB (No recursive archive update required), only returns err
|
// Import has no scope ctx, only import from metafile to DB (No recursive archive update required), only returns err
|
||||||
|
|
||||||
tagId, exists := r.TagId(tagType, tagName, tagScope)
|
tagID, exists := r.TagID(tagType, tagName, tagScope)
|
||||||
if !exists {
|
if !exists {
|
||||||
tagId, err = r.CreateTag(tagType, tagName, tagScope)
|
tagID, err = r.CreateTag(tagType, tagName, tagScope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(jobId, tagId)
|
q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(jobID, tagID)
|
||||||
|
|
||||||
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
|
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
|
||||||
s, _, _ := q.ToSql()
|
s, _, _ := q.ToSql()
|
||||||
@@ -541,6 +616,28 @@ func (r *JobRepository) ImportTag(jobId int64, tagType string, tagName string, t
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkScopeAuth validates whether a user is authorized to perform an operation on a tag with the given scope.
|
||||||
|
//
|
||||||
|
// This function implements the tag scope authorization matrix:
|
||||||
|
//
|
||||||
|
// Scope | Read Access | Write Access
|
||||||
|
// -------------|----------------------------------|----------------------------------
|
||||||
|
// "global" | All users | Admin, Support, API-only
|
||||||
|
// "admin" | Admin, Support | Admin, API-only
|
||||||
|
// <username> | Owner only | Owner only (private tags)
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - user: User attempting the operation (must not be nil)
|
||||||
|
// - operation: Either "read" or "write"
|
||||||
|
// - scope: Tag scope value ("global", "admin", or username for private tags)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - pass: true if authorized, false if denied
|
||||||
|
// - err: error only if operation is invalid or user is nil
|
||||||
|
//
|
||||||
|
// Special cases:
|
||||||
|
// - API-only users (single role: RoleApi) can write to admin and global scopes for automation
|
||||||
|
// - Private tags use the username as scope, granting exclusive access to that user
|
||||||
func (r *JobRepository) checkScopeAuth(user *schema.User, operation string, scope string) (pass bool, err error) {
|
func (r *JobRepository) checkScopeAuth(user *schema.User, operation string, scope string) (pass bool, err error) {
|
||||||
if user != nil {
|
if user != nil {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ func initClusterConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
availability.SubClusters = append(availability.SubClusters, sc.Name)
|
availability.SubClusters = append(availability.SubClusters, sc.Name)
|
||||||
sc.MetricConfig = append(sc.MetricConfig, *newMetric)
|
sc.MetricConfig = append(sc.MetricConfig, newMetric)
|
||||||
|
|
||||||
if newMetric.Footprint != "" {
|
if newMetric.Footprint != "" {
|
||||||
sc.Footprint = append(sc.Footprint, newMetric.Name)
|
sc.Footprint = append(sc.Footprint, newMetric.Name)
|
||||||
@@ -282,7 +282,7 @@ func GetSubClusterByNode(cluster, hostname string) (string, error) {
|
|||||||
return "", fmt.Errorf("ARCHIVE/CLUSTERCONFIG > no subcluster found for cluster %v and host %v", cluster, hostname)
|
return "", fmt.Errorf("ARCHIVE/CLUSTERCONFIG > no subcluster found for cluster %v and host %v", cluster, hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MetricIndex(mc []schema.MetricConfig, name string) (int, error) {
|
func MetricIndex(mc []*schema.MetricConfig, name string) (int, error) {
|
||||||
for i, m := range mc {
|
for i, m := range mc {
|
||||||
if m.Name == name {
|
if m.Name == name {
|
||||||
return i, nil
|
return i, nil
|
||||||
|
|||||||
@@ -3,6 +3,70 @@
|
|||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package archive provides nodelist parsing functionality for HPC cluster node specifications.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The nodelist package implements parsing and querying of compact node list representations
|
||||||
|
// commonly used in HPC job schedulers and cluster management systems. It converts compressed
|
||||||
|
// node specifications (e.g., "node[01-10]") into queryable structures that can efficiently
|
||||||
|
// test node membership and expand to full node lists.
|
||||||
|
//
|
||||||
|
// # Node List Format
|
||||||
|
//
|
||||||
|
// Node lists use a compact syntax with the following rules:
|
||||||
|
//
|
||||||
|
// 1. Comma-separated terms represent alternative node patterns (OR logic)
|
||||||
|
// 2. Each term consists of a string prefix followed by optional numeric ranges
|
||||||
|
// 3. Numeric ranges are specified in square brackets with zero-padded start-end format
|
||||||
|
// 4. Multiple ranges within brackets are comma-separated
|
||||||
|
// 5. Range digits must be zero-padded and of equal length (e.g., "01-99" not "1-99")
|
||||||
|
//
|
||||||
|
// # Examples
|
||||||
|
//
|
||||||
|
// "node01" // Single node
|
||||||
|
// "node01,node02" // Multiple individual nodes
|
||||||
|
// "node[01-10]" // Range: node01 through node10 (zero-padded)
|
||||||
|
// "node[01-10,20-30]" // Multiple ranges: node01-10 and node20-30
|
||||||
|
// "cn-00[10-20],cn-00[50-60]" // Different prefixes with ranges
|
||||||
|
// "login,compute[001-100]" // Mixed individual and range terms
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// Parse a node list specification:
|
||||||
|
//
|
||||||
|
// nl, err := ParseNodeList("node[01-10],login")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Check if a node name matches the list:
|
||||||
|
//
|
||||||
|
// if nl.Contains("node05") {
|
||||||
|
// // node05 is in the list
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Expand to full list of node names:
|
||||||
|
//
|
||||||
|
// nodes := nl.PrintList() // ["node01", "node02", ..., "node10", "login"]
|
||||||
|
//
|
||||||
|
// Count total nodes in the list:
|
||||||
|
//
|
||||||
|
// count := nl.NodeCount() // 11 (10 from range + 1 individual)
|
||||||
|
//
|
||||||
|
// # Integration
|
||||||
|
//
|
||||||
|
// This package is used by:
|
||||||
|
// - clusterConfig.go: Parses SubCluster.Nodes field from cluster configuration
|
||||||
|
// - schema.resolvers.go: GraphQL resolver for computing numberOfNodes in subclusters
|
||||||
|
// - Job archive: Validates node assignments against configured cluster topology
|
||||||
|
//
|
||||||
|
// # Constraints
|
||||||
|
//
|
||||||
|
// - Only zero-padded numeric ranges are supported
|
||||||
|
// - Range start and end must have identical digit counts
|
||||||
|
// - No whitespace allowed in node list specifications
|
||||||
|
// - Ranges must be specified as start-end (not individual numbers)
|
||||||
package archive
|
package archive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -13,12 +77,36 @@ import (
|
|||||||
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NodeList represents a parsed node list specification as a collection of node pattern terms.
|
||||||
|
// Each term is a sequence of expressions that must match consecutively for a node name to match.
|
||||||
|
// Terms are evaluated with OR logic - a node matches if ANY term matches completely.
|
||||||
|
//
|
||||||
|
// Internal structure:
|
||||||
|
// - Outer slice: OR terms (comma-separated in input)
|
||||||
|
// - Inner slice: AND expressions (must all match sequentially)
|
||||||
|
// - Each expression implements: consume (pattern matching), limits (range info), prefix (string part)
|
||||||
|
//
|
||||||
|
// Example: "node[01-10],login" becomes:
|
||||||
|
// - Term 1: [NLExprString("node"), NLExprIntRanges(01-10)]
|
||||||
|
// - Term 2: [NLExprString("login")]
|
||||||
type NodeList [][]interface {
|
type NodeList [][]interface {
|
||||||
consume(input string) (next string, ok bool)
|
consume(input string) (next string, ok bool)
|
||||||
limits() []map[string]int
|
limits() []map[string]int
|
||||||
prefix() string
|
prefix() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contains tests whether the given node name matches any pattern in the NodeList.
|
||||||
|
// Returns true if the name matches at least one term completely, false otherwise.
|
||||||
|
//
|
||||||
|
// Matching logic:
|
||||||
|
// - Evaluates each term sequentially (OR logic across terms)
|
||||||
|
// - Within a term, all expressions must match in order (AND logic)
|
||||||
|
// - A match is complete only if the entire input is consumed (str == "")
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - NodeList("node[01-10]").Contains("node05") → true
|
||||||
|
// - NodeList("node[01-10]").Contains("node11") → false
|
||||||
|
// - NodeList("node[01-10]").Contains("node5") → false (missing zero-padding)
|
||||||
func (nl *NodeList) Contains(name string) bool {
|
func (nl *NodeList) Contains(name string) bool {
|
||||||
var ok bool
|
var ok bool
|
||||||
for _, term := range *nl {
|
for _, term := range *nl {
|
||||||
@@ -38,14 +126,22 @@ func (nl *NodeList) Contains(name string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrintList expands the NodeList into a full slice of individual node names.
|
||||||
|
// This performs the inverse operation of ParseNodeList, expanding all ranges
|
||||||
|
// into their constituent node names with proper zero-padding.
|
||||||
|
//
|
||||||
|
// Returns a slice of node names in the order they appear in the NodeList.
|
||||||
|
// For range terms, nodes are expanded in ascending numeric order.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// - ParseNodeList("node[01-03],login").PrintList() → ["node01", "node02", "node03", "login"]
|
||||||
func (nl *NodeList) PrintList() []string {
|
func (nl *NodeList) PrintList() []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, term := range *nl {
|
for _, term := range *nl {
|
||||||
// Get String-Part first
|
|
||||||
prefix := term[0].prefix()
|
prefix := term[0].prefix()
|
||||||
if len(term) == 1 { // If only String-Part in Term: Single Node Name -> Use as provided
|
if len(term) == 1 {
|
||||||
out = append(out, prefix)
|
out = append(out, prefix)
|
||||||
} else { // Else: Numeric start-end definition with x digits zeroPadded
|
} else {
|
||||||
limitArr := term[1].limits()
|
limitArr := term[1].limits()
|
||||||
for _, inner := range limitArr {
|
for _, inner := range limitArr {
|
||||||
for i := inner["start"]; i < inner["end"]+1; i++ {
|
for i := inner["start"]; i < inner["end"]+1; i++ {
|
||||||
@@ -61,12 +157,22 @@ func (nl *NodeList) PrintList() []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NodeCount returns the total number of individual nodes represented by the NodeList.
|
||||||
|
// This efficiently counts nodes without expanding the full list, making it suitable
|
||||||
|
// for large node ranges.
|
||||||
|
//
|
||||||
|
// Calculation:
|
||||||
|
// - Individual node terms contribute 1
|
||||||
|
// - Range terms contribute (end - start + 1) for each range
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// - ParseNodeList("node[01-10],login").NodeCount() → 11 (10 from range + 1 individual)
|
||||||
func (nl *NodeList) NodeCount() int {
|
func (nl *NodeList) NodeCount() int {
|
||||||
out := 0
|
out := 0
|
||||||
for _, term := range *nl {
|
for _, term := range *nl {
|
||||||
if len(term) == 1 { // If only String-Part in Term: Single Node Name -> add one
|
if len(term) == 1 {
|
||||||
out += 1
|
out += 1
|
||||||
} else { // Else: Numeric start-end definition -> add difference + 1
|
} else {
|
||||||
limitArr := term[1].limits()
|
limitArr := term[1].limits()
|
||||||
for _, inner := range limitArr {
|
for _, inner := range limitArr {
|
||||||
out += (inner["end"] - inner["start"]) + 1
|
out += (inner["end"] - inner["start"]) + 1
|
||||||
@@ -76,6 +182,8 @@ func (nl *NodeList) NodeCount() int {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NLExprString represents a literal string prefix in a node name pattern.
|
||||||
|
// It matches by checking if the input starts with this exact string.
|
||||||
type NLExprString string
|
type NLExprString string
|
||||||
|
|
||||||
func (nle NLExprString) consume(input string) (next string, ok bool) {
|
func (nle NLExprString) consume(input string) (next string, ok bool) {
|
||||||
@@ -96,6 +204,8 @@ func (nle NLExprString) prefix() string {
|
|||||||
return string(nle)
|
return string(nle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NLExprIntRanges represents multiple alternative integer ranges (comma-separated within brackets).
|
||||||
|
// A node name matches if it matches ANY of the contained ranges (OR logic).
|
||||||
type NLExprIntRanges []NLExprIntRange
|
type NLExprIntRanges []NLExprIntRange
|
||||||
|
|
||||||
func (nles NLExprIntRanges) consume(input string) (next string, ok bool) {
|
func (nles NLExprIntRanges) consume(input string) (next string, ok bool) {
|
||||||
@@ -122,6 +232,11 @@ func (nles NLExprIntRanges) prefix() string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NLExprIntRange represents a single zero-padded integer range (e.g., "01-99").
|
||||||
|
// Fields:
|
||||||
|
// - start, end: Numeric range boundaries (inclusive)
|
||||||
|
// - zeroPadded: Must be true (non-padded ranges not supported)
|
||||||
|
// - digits: Required digit count for zero-padding
|
||||||
type NLExprIntRange struct {
|
type NLExprIntRange struct {
|
||||||
start, end int64
|
start, end int64
|
||||||
zeroPadded bool
|
zeroPadded bool
|
||||||
@@ -176,6 +291,28 @@ func (nles NLExprIntRange) prefix() string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseNodeList parses a compact node list specification into a queryable NodeList structure.
|
||||||
|
//
|
||||||
|
// Input format rules:
|
||||||
|
// - Comma-separated terms (OR logic): "node01,node02" matches either node
|
||||||
|
// - Range syntax: "node[01-10]" expands to node01 through node10
|
||||||
|
// - Multiple ranges: "node[01-05,10-15]" creates two ranges
|
||||||
|
// - Zero-padding required: digits in ranges must be zero-padded and equal length
|
||||||
|
// - Mixed formats: "login,compute[001-100]" combines individual and range terms
|
||||||
|
//
|
||||||
|
// Validation:
|
||||||
|
// - Returns error if brackets are unclosed
|
||||||
|
// - Returns error if ranges lack '-' separator
|
||||||
|
// - Returns error if range digits have unequal length
|
||||||
|
// - Returns error if range numbers fail to parse
|
||||||
|
// - Returns error on invalid characters
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - "node[01-10]" → NodeList with one term (10 nodes)
|
||||||
|
// - "node01,node02" → NodeList with two terms (2 nodes)
|
||||||
|
// - "cn[01-05,10-15]" → NodeList with ranges 01-05 and 10-15 (11 nodes total)
|
||||||
|
// - "a[1-9]" → Error (not zero-padded)
|
||||||
|
// - "a[01-9]" → Error (unequal digit counts)
|
||||||
func ParseNodeList(raw string) (NodeList, error) {
|
func ParseNodeList(raw string) (NodeList, error) {
|
||||||
isLetter := func(r byte) bool { return ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') }
|
isLetter := func(r byte) bool { return ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') }
|
||||||
isDigit := func(r byte) bool { return '0' <= r && r <= '9' }
|
isDigit := func(r byte) bool { return '0' <= r && r <= '9' }
|
||||||
@@ -232,12 +369,12 @@ func ParseNodeList(raw string) (NodeList, error) {
|
|||||||
nles := NLExprIntRanges{}
|
nles := NLExprIntRanges{}
|
||||||
|
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
minus := strings.Index(part, "-")
|
before, after, ok := strings.Cut(part, "-")
|
||||||
if minus == -1 {
|
if !ok {
|
||||||
return nil, fmt.Errorf("ARCHIVE/NODELIST > no '-' found inside '[...]'")
|
return nil, fmt.Errorf("ARCHIVE/NODELIST > no '-' found inside '[...]'")
|
||||||
}
|
}
|
||||||
|
|
||||||
s1, s2 := part[0:minus], part[minus+1:]
|
s1, s2 := before, after
|
||||||
if len(s1) != len(s2) || len(s1) == 0 {
|
if len(s1) != len(s2) || len(s1) == 0 {
|
||||||
return nil, fmt.Errorf("ARCHIVE/NODELIST > %v and %v are not of equal length or of length zero", s1, s2)
|
return nil, fmt.Errorf("ARCHIVE/NODELIST > %v and %v are not of equal length or of length zero", s1, s2)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user