[release] v0.22.19

This commit is contained in:
Yann Stepienik
2026-05-26 12:59:20 +01:00
parent 88de73dcca
commit 59c561d686
31 changed files with 1514 additions and 411 deletions
+161 -1
View File
@@ -4,7 +4,7 @@
"description": "REST API for Cosmos Cloud server management",
"title": "Cosmos Server API",
"contact": {},
"version": "0.22.18"
"version": "0.22.19"
},
"paths": {
"/_logs": {
@@ -2307,6 +2307,52 @@
}
}
},
"/api/constellation/deployments/health": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Returns desired vs. actual replica placement per deployment plus\nscheduler quarantine state (Pro feature). Read-only: safe to call\nfrom any node, not just the scheduler leader.",
"tags": [
"deployments"
],
"summary": "Cluster health for all deployments",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.APIResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.HTTPErrorResult"
}
}
}
},
"503": {
"description": "Service Unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.HTTPErrorResult"
}
}
}
}
}
}
},
"/api/constellation/deployments/{name}": {
"get": {
"security": [
@@ -2495,6 +2541,63 @@
}
}
},
"/api/constellation/deployments/{name}/unbroke": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Removes a deployment from the scheduler's quarantine set so the\nnext reconcile cycle attempts to place it again (Pro feature).",
"tags": [
"deployments"
],
"summary": "Clear a deployment's quarantine",
"parameters": [
{
"description": "Deployment name",
"name": "name",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.APIResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.HTTPErrorResult"
}
}
}
},
"405": {
"description": "Method Not Allowed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.HTTPErrorResult"
}
}
}
}
}
}
},
"/api/constellation/devices": {
"get": {
"security": [
@@ -3100,6 +3203,63 @@
}
}
},
"/api/constellation/nodes/{name}/unbroke": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Removes a node from the scheduler's quarantine set and resets its\nfail streak so the next reconcile cycle is free to place work on it\nagain (Pro feature).",
"tags": [
"deployments"
],
"summary": "Clear a node's quarantine",
"parameters": [
{
"description": "Node (device) name",
"name": "name",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.APIResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.HTTPErrorResult"
}
}
}
},
"405": {
"description": "Method Not Allowed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/utils.HTTPErrorResult"
}
}
}
}
}
}
},
"/api/constellation/ping": {
"get": {
"security": [
+130 -1
View File
@@ -4,7 +4,7 @@
"description": "REST API for Cosmos Cloud server management",
"title": "Cosmos Server API",
"contact": {},
"version": "0.22.18"
"version": "0.22.19"
},
"basePath": "/cosmos",
"paths": {
@@ -1904,6 +1904,43 @@
}
}
},
"/api/constellation/deployments/health": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Returns desired vs. actual replica placement per deployment plus\nscheduler quarantine state (Pro feature). Read-only: safe to call\nfrom any node, not just the scheduler leader.",
"produces": [
"application/json"
],
"tags": [
"deployments"
],
"summary": "Cluster health for all deployments",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/utils.HTTPErrorResult"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/utils.HTTPErrorResult"
}
}
}
}
},
"/api/constellation/deployments/{name}": {
"get": {
"security": [
@@ -2056,6 +2093,52 @@
}
}
},
"/api/constellation/deployments/{name}/unbroke": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Removes a deployment from the scheduler's quarantine set so the\nnext reconcile cycle attempts to place it again (Pro feature).",
"produces": [
"application/json"
],
"tags": [
"deployments"
],
"summary": "Clear a deployment's quarantine",
"parameters": [
{
"type": "string",
"description": "Deployment name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/utils.HTTPErrorResult"
}
},
"405": {
"description": "Method Not Allowed",
"schema": {
"$ref": "#/definitions/utils.HTTPErrorResult"
}
}
}
}
},
"/api/constellation/devices": {
"get": {
"security": [
@@ -2552,6 +2635,52 @@
}
}
},
"/api/constellation/nodes/{name}/unbroke": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Removes a node from the scheduler's quarantine set and resets its\nfail streak so the next reconcile cycle is free to place work on it\nagain (Pro feature).",
"produces": [
"application/json"
],
"tags": [
"deployments"
],
"summary": "Clear a node's quarantine",
"parameters": [
{
"type": "string",
"description": "Node (device) name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/utils.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/utils.HTTPErrorResult"
}
},
"405": {
"description": "Method Not Allowed",
"schema": {
"$ref": "#/definitions/utils.HTTPErrorResult"
}
}
}
}
},
"/api/constellation/ping": {
"get": {
"security": [
+90 -1
View File
@@ -2057,7 +2057,7 @@ info:
contact: {}
description: REST API for Cosmos Cloud server management
title: Cosmos Server API
version: 0.22.18
version: 0.22.19
paths:
/_logs:
get:
@@ -3368,6 +3368,63 @@ paths:
summary: Update a cluster deployment
tags:
- deployments
/api/constellation/deployments/{name}/unbroke:
post:
description: |-
Removes a deployment from the scheduler's quarantine set so the
next reconcile cycle attempts to place it again (Pro feature).
parameters:
- description: Deployment name
in: path
name: name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/utils.HTTPErrorResult'
"405":
description: Method Not Allowed
schema:
$ref: '#/definitions/utils.HTTPErrorResult'
security:
- BearerAuth: []
summary: Clear a deployment's quarantine
tags:
- deployments
/api/constellation/deployments/health:
get:
description: |-
Returns desired vs. actual replica placement per deployment plus
scheduler quarantine state (Pro feature). Read-only: safe to call
from any node, not just the scheduler leader.
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/utils.HTTPErrorResult'
"503":
description: Service Unavailable
schema:
$ref: '#/definitions/utils.HTTPErrorResult'
security:
- BearerAuth: []
summary: Cluster health for all deployments
tags:
- deployments
/api/constellation/devices:
get:
produces:
@@ -3677,6 +3734,38 @@ paths:
summary: Get Nebula VPN service logs
tags:
- constellation
/api/constellation/nodes/{name}/unbroke:
post:
description: |-
Removes a node from the scheduler's quarantine set and resets its
fail streak so the next reconcile cycle is free to place work on it
again (Pro feature).
parameters:
- description: Node (device) name
in: path
name: name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/utils.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/utils.HTTPErrorResult'
"405":
description: Method Not Allowed
schema:
$ref: '#/definitions/utils.HTTPErrorResult'
security:
- BearerAuth: []
summary: Clear a node's quarantine
tags:
- deployments
/api/constellation/ping:
get:
produces:
+7
View File
@@ -1,3 +1,10 @@
## Version 0.22.19
- Only allow manager server to bypass tunnel authorization
- Add authorization check on legacy Public Device endpoint
- Pro SDK generation
- cosmos-compose now adds a default container name when missing
- Attempt at fixing all MacOS Safari login issues on bare IP
## Version 0.22.18
- More Terraform improvemnts
+33 -1
View File
@@ -3,9 +3,20 @@ import wrap, { type ApiResponse, type ApiFetch } from './wrap';
export interface Deployment {
name: string;
replicas: number;
strategy?: 'round-robin' | 'least-busy';
tags?: string[];
storage?: string[];
compose: any;
}
export interface DeploymentHealth {
desired: number;
actual: number;
nodes: string[];
broken: boolean;
brokenReason?: string;
}
export default function createDeploymentsAPI(apiFetch: ApiFetch) {
function list(): Promise<ApiResponse<Record<string, Deployment>>> {
return wrap(apiFetch('/cosmos/api/constellation/deployments', {
@@ -44,5 +55,26 @@ export default function createDeploymentsAPI(apiFetch: ApiFetch) {
}));
}
return { list, get, create, update, remove };
function health(): Promise<ApiResponse<Record<string, DeploymentHealth>> & { brokenNodes?: Record<string, string> }> {
return wrap(apiFetch('/cosmos/api/constellation/deployments/health', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}));
}
function unbroke(name: string): Promise<ApiResponse> {
return wrap(apiFetch('/cosmos/api/constellation/deployments/' + name + '/unbroke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}));
}
function unbrokeNode(name: string): Promise<ApiResponse> {
return wrap(apiFetch('/cosmos/api/constellation/nodes/' + name + '/unbroke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}));
}
return { list, get, create, update, remove, health, unbroke, unbrokeNode };
}
+2
View File
@@ -395,6 +395,7 @@ let backups = createBackupsAPI(defaultFetch);
let apiTokens = createApiTokensAPI(defaultFetch);
let openid = createOpenIDAPI(defaultFetch);
let groups = createGroupsAPI(defaultFetch);
let deployments = createDeploymentsAPI(defaultFetch);
if(isDemo) {
auth = authDemo;
@@ -441,4 +442,5 @@ export {
apiTokens,
openid,
groups,
deployments,
};
+23 -20
View File
@@ -10,6 +10,7 @@ import PrettyTabbedView from '../../components/tabbedView/tabbedView';
import { useClientInfos } from '../../utils/hooks';
import { PERM_RESOURCES_READ } from '../../utils/permissions';
import { useTranslation } from 'react-i18next';
import proFeatures from '../../pro';
import { ConstellationVPN } from './vpn';
@@ -34,27 +35,29 @@ const ConstellationIndex = () => {
const freeVersion = coStatus && !coStatus.Licence;
const tabs = [
{
title: 'VPN',
children: <ConstellationVPN freeVersion={freeVersion} />,
path: 'vpn'
},
{
title: 'DNS',
children: <ConstellationDNS />,
path: 'dns'
}
];
if (proFeatures.isPro && proFeatures.isPro() && proFeatures.DeploymentsTab) {
tabs.push({
title: t('mgmt.deployments.tabTitle'),
children: <proFeatures.DeploymentsTab />,
path: 'deployments',
});
}
const ConstContent = isAdmin && !freeVersion ? <div>
<PrettyTabbedView path="/cosmos-ui/constellation/:tab" tabs={[
{
title: 'VPN',
children: <ConstellationVPN freeVersion={freeVersion} />,
path: 'vpn'
},
{
title: 'DNS',
children: <ConstellationDNS />,
path: 'dns'
},
{
title: t('mgmt.constellation.setup.unsafeRoutesTitle'),
children: <div>
<Alert severity="info">
{t('mgmt.constellation.setup.unsafeRoutesText')}
</Alert>
</div>,
}
]}/>
<PrettyTabbedView path="/cosmos-ui/constellation/:tab" tabs={tabs}/>
</div> : <ConstellationVPN freeVersion={freeVersion} />;
+13 -1
View File
@@ -37,6 +37,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
const [devicePingStatus, setDevicePingStatus] = useState({}); // {deviceName: 'loading' | 'success' | 'error'}
const [firewallLoading, setFirewallLoading] = useState(null); // deviceName being toggled
const [currentDeviceName, setCurrentDeviceName] = useState('');
const [leader, setLeader] = useState(''); // sanitized device name of the cluster scheduler leader, '' when unknown
const [enableLoading, setEnableLoading] = useState(false);
const refreshStatus = () => {
@@ -119,6 +120,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
const deviceList = listRes.data || [];
setDevices(deviceList);
setCurrentDeviceName(listRes.currentDeviceName || '');
setLeader(listRes.leader || '');
if (isAdmin)
setUsers((await API.users.list()).data || []);
else
@@ -162,6 +164,12 @@ export const ConstellationVPN = ({ freeVersion }) => {
return <Tooltip title={label}>{icon}</Tooltip>;
}
// The backend stores the leader under its sanitized device name (see
// sanitizeNATSUsername): space . - : / \ all become '_'. We sanitize each
// row the same way to match rather than reversing it server-side.
const sanitizeName = (s) => (s || '').replace(/[ .:/\\-]/g, '_');
const isLeaderDevice = (r) => !!leader && r && sanitizeName(r.deviceName) === leader;
const currentDevice = devices && devices.find(d => d.deviceName === currentDeviceName);
return <>
@@ -197,6 +205,7 @@ export const ConstellationVPN = ({ freeVersion }) => {
{currentDevice && <>
<Stack direction="row" spacing={1} alignItems="center" sx={{ color: 'text.secondary' }}>
{isLeaderDevice(currentDevice) && <Tooltip title={t('mgmt.constellation.leaderTooltip')}><Chip size="small" color="primary" label={t('mgmt.constellation.leader')} /></Tooltip>}
{currentDevice.isLighthouse
? <Tooltip title={t('mgmt.constellation.publicLighthouseTooltip')}><CompassOutlined style={{ fontSize: 16 }} /></Tooltip>
: <Tooltip title={t('mgmt.constellation.privateServerTooltip')}><DesktopOutlined style={{ fontSize: 16, opacity: 0.5 }} /></Tooltip>
@@ -621,7 +630,10 @@ export const ConstellationVPN = ({ freeVersion }) => {
return <div style={{display: "flex", alignItems: "center", gap: "8px"}}>
{res}
<div>
<div><strong>{r.deviceName}</strong></div>
<div style={{display: "flex", alignItems: "center", gap: "6px"}}>
<strong>{r.deviceName}</strong>
{isLeaderDevice(r) && <Tooltip title={t('mgmt.constellation.leaderTooltip')}><Chip size="small" color="primary" label={t('mgmt.constellation.leader')} /></Tooltip>}
</div>
<div style={{opacity: 0.8}}>{r.ip}</div>
</div>
</div>
+19 -18
View File
@@ -18,25 +18,26 @@ const ServappsIndex = () => {
const { t } = useTranslation();
const { stack } = useParams();
const tabs = [
{
title: t('mgmt.servapps.networks.containers'),
children: <ServApps stack={stack} />,
path: 'containers'
},
{
title: t('mgmt.servapps.networks.volumes'),
children: <VolumeManagementList />,
path: 'volumes'
},
{
title: t('global.networks'),
children: <NetworkManagementList />,
path: 'networks'
},
];
return <div>
{!stack && <PrettyTabbedView path="/cosmos-ui/servapps/:tab" tabs={[
{
title: t('mgmt.servapps.networks.containers'),
children: <ServApps stack={stack} />,
path: 'containers'
},
{
title: t('mgmt.servapps.networks.volumes'),
children: <VolumeManagementList />,
path: 'volumes'
},
{
title: t('global.networks'),
children: <NetworkManagementList />,
path: 'networks'
},
]}/>
}
{!stack && <PrettyTabbedView path="/cosmos-ui/servapps/:tab" tabs={tabs}/>}
{stack && <ServApps stack={stack} />}
+2 -1
View File
@@ -3,5 +3,6 @@ export default {
GroupsTab: null,
RouteLBSettings: null,
ConstellationNATSReplicas: null,
DeploymentForm: null
DeploymentForm: null,
DeploymentsTab: null
}
@@ -399,6 +399,8 @@
"mgmt.constellation.cosmosNodeAgentTooltip": "This server is an agent node managed by another Cosmos server",
"mgmt.constellation.lighthouse": "Lighthouse",
"mgmt.constellation.client": "Client",
"mgmt.constellation.leader": "Leader",
"mgmt.constellation.leaderTooltip": "Current cluster scheduler leader — runs the deployment reconciliation loop",
"mgmt.constellation.updating": "Updating...",
"mgmt.constellation.blocked": "Blocked",
"mgmt.constellation.allowed": "Allowed",
@@ -434,6 +436,21 @@
"mgmt.deployments.replicasMin": "Replicas must be at least 1",
"mgmt.deployments.invalidJson": "Invalid JSON in compose field",
"mgmt.deployments.createError": "Error creating deployment",
"mgmt.deployments.updateError": "Error updating deployment",
"mgmt.deployments.saveButton": "Save Changes",
"mgmt.deployments.tabTitle": "Deployments",
"mgmt.deployments.createTitle": "Create Deployment",
"mgmt.deployments.editTitle": "Edit Deployment",
"mgmt.deployments.deleteConfirm": "Delete deployment \"{{name}}\"? The scheduler will stop and remove all of its replicas across the cluster.",
"mgmt.deployments.nodesHeader": "Nodes",
"mgmt.deployments.statusHeader": "Status",
"mgmt.deployments.broken": "Quarantined",
"mgmt.deployments.unbroke": "Clear quarantine",
"mgmt.deployments.degraded": "Degraded",
"mgmt.deployments.degradedTooltip": "Only {{actual}} of {{desired}} replicas are running",
"mgmt.deployments.brokenNodesTitle": "Quarantined nodes",
"mgmt.deployments.brokenNodesDesc": "These nodes failed repeated deploys and are skipped by the scheduler until cleared, which can leave deployments under-replicated.",
"mgmt.deployments.unbrokeNode": "Clear node quarantine",
"mgmt.cron.editCron.customText": "Create a custom job to run a shell command in a container. Leave the container field empty to run on the host",
"mgmt.cron.editCron.customText.onHostOnly": "Running on the host only works if Cosmos is not itself running in a container",
"mgmt.cron.editCronTitle": "Edit Job",
+580 -183
View File
@@ -1981,6 +1981,9 @@ type ClientInterface interface {
PostApiConstellationDeployments(ctx context.Context, body PostApiConstellationDeploymentsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// GetApiConstellationDeploymentsHealth request
GetApiConstellationDeploymentsHealth(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// DeleteApiConstellationDeploymentsName request
DeleteApiConstellationDeploymentsName(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -1992,6 +1995,9 @@ type ClientInterface interface {
PutApiConstellationDeploymentsName(ctx context.Context, name string, body PutApiConstellationDeploymentsNameJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// PostApiConstellationDeploymentsNameUnbroke request
PostApiConstellationDeploymentsNameUnbroke(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*http.Response, error)
// GetApiConstellationDevices request
GetApiConstellationDevices(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -2033,6 +2039,9 @@ type ClientInterface interface {
// GetApiConstellationLogs request
GetApiConstellationLogs(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// PostApiConstellationNodesNameUnbroke request
PostApiConstellationNodesNameUnbroke(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*http.Response, error)
// GetApiConstellationPing request
GetApiConstellationPing(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -3100,6 +3109,18 @@ func (c *Client) PostApiConstellationDeployments(ctx context.Context, body PostA
return c.Client.Do(req)
}
func (c *Client) GetApiConstellationDeploymentsHealth(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewGetApiConstellationDeploymentsHealthRequest(c.Server)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) DeleteApiConstellationDeploymentsName(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewDeleteApiConstellationDeploymentsNameRequest(c.Server, name)
if err != nil {
@@ -3148,6 +3169,18 @@ func (c *Client) PutApiConstellationDeploymentsName(ctx context.Context, name st
return c.Client.Do(req)
}
func (c *Client) PostApiConstellationDeploymentsNameUnbroke(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostApiConstellationDeploymentsNameUnbrokeRequest(c.Server, name)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) GetApiConstellationDevices(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewGetApiConstellationDevicesRequest(c.Server)
if err != nil {
@@ -3328,6 +3361,18 @@ func (c *Client) GetApiConstellationLogs(ctx context.Context, reqEditors ...Requ
return c.Client.Do(req)
}
func (c *Client) PostApiConstellationNodesNameUnbroke(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostApiConstellationNodesNameUnbrokeRequest(c.Server, name)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) GetApiConstellationPing(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewGetApiConstellationPingRequest(c.Server)
if err != nil {
@@ -6592,6 +6637,33 @@ func NewPostApiConstellationDeploymentsRequestWithBody(server string, contentTyp
return req, nil
}
// NewGetApiConstellationDeploymentsHealthRequest generates requests for GetApiConstellationDeploymentsHealth
func NewGetApiConstellationDeploymentsHealthRequest(server string) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/api/constellation/deployments/health")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewDeleteApiConstellationDeploymentsNameRequest generates requests for DeleteApiConstellationDeploymentsName
func NewDeleteApiConstellationDeploymentsNameRequest(server string, name string) (*http.Request, error) {
var err error
@@ -6707,6 +6779,40 @@ func NewPutApiConstellationDeploymentsNameRequestWithBody(server string, name st
return req, nil
}
// NewPostApiConstellationDeploymentsNameUnbrokeRequest generates requests for PostApiConstellationDeploymentsNameUnbroke
func NewPostApiConstellationDeploymentsNameUnbrokeRequest(server string, name string) (*http.Request, error) {
var err error
var pathParam0 string
pathParam0, err = runtime.StyleParamWithOptions("simple", false, "name", name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""})
if err != nil {
return nil, err
}
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/api/constellation/deployments/%s/unbroke", pathParam0)
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewGetApiConstellationDevicesRequest generates requests for GetApiConstellationDevices
func NewGetApiConstellationDevicesRequest(server string) (*http.Request, error) {
var err error
@@ -7084,6 +7190,40 @@ func NewGetApiConstellationLogsRequest(server string) (*http.Request, error) {
return req, nil
}
// NewPostApiConstellationNodesNameUnbrokeRequest generates requests for PostApiConstellationNodesNameUnbroke
func NewPostApiConstellationNodesNameUnbrokeRequest(server string, name string) (*http.Request, error) {
var err error
var pathParam0 string
pathParam0, err = runtime.StyleParamWithOptions("simple", false, "name", name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""})
if err != nil {
return nil, err
}
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/api/constellation/nodes/%s/unbroke", pathParam0)
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewGetApiConstellationPingRequest generates requests for GetApiConstellationPing
func NewGetApiConstellationPingRequest(server string) (*http.Request, error) {
var err error
@@ -11739,6 +11879,9 @@ type ClientWithResponsesInterface interface {
PostApiConstellationDeploymentsWithResponse(ctx context.Context, body PostApiConstellationDeploymentsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiConstellationDeploymentsResponse, error)
// GetApiConstellationDeploymentsHealthWithResponse request
GetApiConstellationDeploymentsHealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiConstellationDeploymentsHealthResponse, error)
// DeleteApiConstellationDeploymentsNameWithResponse request
DeleteApiConstellationDeploymentsNameWithResponse(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*DeleteApiConstellationDeploymentsNameResponse, error)
@@ -11750,6 +11893,9 @@ type ClientWithResponsesInterface interface {
PutApiConstellationDeploymentsNameWithResponse(ctx context.Context, name string, body PutApiConstellationDeploymentsNameJSONRequestBody, reqEditors ...RequestEditorFn) (*PutApiConstellationDeploymentsNameResponse, error)
// PostApiConstellationDeploymentsNameUnbrokeWithResponse request
PostApiConstellationDeploymentsNameUnbrokeWithResponse(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*PostApiConstellationDeploymentsNameUnbrokeResponse, error)
// GetApiConstellationDevicesWithResponse request
GetApiConstellationDevicesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiConstellationDevicesResponse, error)
@@ -11791,6 +11937,9 @@ type ClientWithResponsesInterface interface {
// GetApiConstellationLogsWithResponse request
GetApiConstellationLogsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiConstellationLogsResponse, error)
// PostApiConstellationNodesNameUnbrokeWithResponse request
PostApiConstellationNodesNameUnbrokeWithResponse(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*PostApiConstellationNodesNameUnbrokeResponse, error)
// GetApiConstellationPingWithResponse request
GetApiConstellationPingWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiConstellationPingResponse, error)
@@ -13403,6 +13552,38 @@ func (r PostApiConstellationDeploymentsResponse) ContentType() string {
return ""
}
type GetApiConstellationDeploymentsHealthResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *UtilsAPIResponse
JSON401 *UtilsHTTPErrorResult
JSON503 *UtilsHTTPErrorResult
}
// Status returns HTTPResponse.Status
func (r GetApiConstellationDeploymentsHealthResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r GetApiConstellationDeploymentsHealthResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers
func (r GetApiConstellationDeploymentsHealthResponse) ContentType() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Header.Get("Content-Type")
}
return ""
}
type DeleteApiConstellationDeploymentsNameResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -13500,6 +13681,38 @@ func (r PutApiConstellationDeploymentsNameResponse) ContentType() string {
return ""
}
type PostApiConstellationDeploymentsNameUnbrokeResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *UtilsAPIResponse
JSON401 *UtilsHTTPErrorResult
JSON405 *UtilsHTTPErrorResult
}
// Status returns HTTPResponse.Status
func (r PostApiConstellationDeploymentsNameUnbrokeResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r PostApiConstellationDeploymentsNameUnbrokeResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers
func (r PostApiConstellationDeploymentsNameUnbrokeResponse) ContentType() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Header.Get("Content-Type")
}
return ""
}
type GetApiConstellationDevicesResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -13855,6 +14068,38 @@ func (r GetApiConstellationLogsResponse) ContentType() string {
return ""
}
type PostApiConstellationNodesNameUnbrokeResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *UtilsAPIResponse
JSON401 *UtilsHTTPErrorResult
JSON405 *UtilsHTTPErrorResult
}
// Status returns HTTPResponse.Status
func (r PostApiConstellationNodesNameUnbrokeResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r PostApiConstellationNodesNameUnbrokeResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers
func (r PostApiConstellationNodesNameUnbrokeResponse) ContentType() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Header.Get("Content-Type")
}
return ""
}
type GetApiConstellationPingResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -18050,6 +18295,15 @@ func (c *ClientWithResponses) PostApiConstellationDeploymentsWithResponse(ctx co
return ParsePostApiConstellationDeploymentsResponse(rsp)
}
// GetApiConstellationDeploymentsHealthWithResponse request returning *GetApiConstellationDeploymentsHealthResponse
func (c *ClientWithResponses) GetApiConstellationDeploymentsHealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiConstellationDeploymentsHealthResponse, error) {
rsp, err := c.GetApiConstellationDeploymentsHealth(ctx, reqEditors...)
if err != nil {
return nil, err
}
return ParseGetApiConstellationDeploymentsHealthResponse(rsp)
}
// DeleteApiConstellationDeploymentsNameWithResponse request returning *DeleteApiConstellationDeploymentsNameResponse
func (c *ClientWithResponses) DeleteApiConstellationDeploymentsNameWithResponse(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*DeleteApiConstellationDeploymentsNameResponse, error) {
rsp, err := c.DeleteApiConstellationDeploymentsName(ctx, name, reqEditors...)
@@ -18085,6 +18339,15 @@ func (c *ClientWithResponses) PutApiConstellationDeploymentsNameWithResponse(ctx
return ParsePutApiConstellationDeploymentsNameResponse(rsp)
}
// PostApiConstellationDeploymentsNameUnbrokeWithResponse request returning *PostApiConstellationDeploymentsNameUnbrokeResponse
func (c *ClientWithResponses) PostApiConstellationDeploymentsNameUnbrokeWithResponse(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*PostApiConstellationDeploymentsNameUnbrokeResponse, error) {
rsp, err := c.PostApiConstellationDeploymentsNameUnbroke(ctx, name, reqEditors...)
if err != nil {
return nil, err
}
return ParsePostApiConstellationDeploymentsNameUnbrokeResponse(rsp)
}
// GetApiConstellationDevicesWithResponse request returning *GetApiConstellationDevicesResponse
func (c *ClientWithResponses) GetApiConstellationDevicesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiConstellationDevicesResponse, error) {
rsp, err := c.GetApiConstellationDevices(ctx, reqEditors...)
@@ -18216,6 +18479,15 @@ func (c *ClientWithResponses) GetApiConstellationLogsWithResponse(ctx context.Co
return ParseGetApiConstellationLogsResponse(rsp)
}
// PostApiConstellationNodesNameUnbrokeWithResponse request returning *PostApiConstellationNodesNameUnbrokeResponse
func (c *ClientWithResponses) PostApiConstellationNodesNameUnbrokeWithResponse(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*PostApiConstellationNodesNameUnbrokeResponse, error) {
rsp, err := c.PostApiConstellationNodesNameUnbroke(ctx, name, reqEditors...)
if err != nil {
return nil, err
}
return ParsePostApiConstellationNodesNameUnbrokeResponse(rsp)
}
// GetApiConstellationPingWithResponse request returning *GetApiConstellationPingResponse
func (c *ClientWithResponses) GetApiConstellationPingWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiConstellationPingResponse, error) {
rsp, err := c.GetApiConstellationPing(ctx, reqEditors...)
@@ -21216,6 +21488,46 @@ func ParsePostApiConstellationDeploymentsResponse(rsp *http.Response) (*PostApiC
return response, nil
}
// ParseGetApiConstellationDeploymentsHealthResponse parses an HTTP response from a GetApiConstellationDeploymentsHealthWithResponse call
func ParseGetApiConstellationDeploymentsHealthResponse(rsp *http.Response) (*GetApiConstellationDeploymentsHealthResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &GetApiConstellationDeploymentsHealthResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest UtilsAPIResponse
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
var dest UtilsHTTPErrorResult
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON401 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503:
var dest UtilsHTTPErrorResult
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON503 = &dest
}
return response, nil
}
// ParseDeleteApiConstellationDeploymentsNameResponse parses an HTTP response from a DeleteApiConstellationDeploymentsNameWithResponse call
func ParseDeleteApiConstellationDeploymentsNameResponse(rsp *http.Response) (*DeleteApiConstellationDeploymentsNameResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
@@ -21343,6 +21655,46 @@ func ParsePutApiConstellationDeploymentsNameResponse(rsp *http.Response) (*PutAp
return response, nil
}
// ParsePostApiConstellationDeploymentsNameUnbrokeResponse parses an HTTP response from a PostApiConstellationDeploymentsNameUnbrokeWithResponse call
func ParsePostApiConstellationDeploymentsNameUnbrokeResponse(rsp *http.Response) (*PostApiConstellationDeploymentsNameUnbrokeResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &PostApiConstellationDeploymentsNameUnbrokeResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest UtilsAPIResponse
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
var dest UtilsHTTPErrorResult
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON401 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 405:
var dest UtilsHTTPErrorResult
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON405 = &dest
}
return response, nil
}
// ParseGetApiConstellationDevicesResponse parses an HTTP response from a GetApiConstellationDevicesWithResponse call
func ParseGetApiConstellationDevicesResponse(rsp *http.Response) (*GetApiConstellationDevicesResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
@@ -21804,6 +22156,46 @@ func ParseGetApiConstellationLogsResponse(rsp *http.Response) (*GetApiConstellat
return response, nil
}
// ParsePostApiConstellationNodesNameUnbrokeResponse parses an HTTP response from a PostApiConstellationNodesNameUnbrokeWithResponse call
func ParsePostApiConstellationNodesNameUnbrokeResponse(rsp *http.Response) (*PostApiConstellationNodesNameUnbrokeResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &PostApiConstellationNodesNameUnbrokeResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest UtilsAPIResponse
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
var dest UtilsHTTPErrorResult
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON401 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 405:
var dest UtilsHTTPErrorResult
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON405 = &dest
}
return response, nil
}
// ParseGetApiConstellationPingResponse parses an HTTP response from a GetApiConstellationPingWithResponse call
func ParseGetApiConstellationPingResponse(rsp *http.Response) (*GetApiConstellationPingResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
@@ -26723,7 +27115,7 @@ func ParsePostRcloneVfsStatsResponse(rsp *http.Response) (*PostRcloneVfsStatsRes
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
"7L3rbhs51ij6KoTOBtoBZDlJ93z4JsD+4fjS7RnfIDkZ7D0OsqmqJYntKrKGZMlRNwKchzhPeJ5kg4us",
"7L3rbhs51ij6KoTOBtoBZDlJ93zYE2D/cHzp9oxvkJwM9h4H+aiqJYntKrKGZMlRNwKchzhPeJ5kg4us",
"UpVESlWyndjp+jHTsYp3rhvX9c9eJNJMcOBa9d792ZPwnxyUfi9iBvjDexEvzH8jwTVwbf5JsyxhEdVM",
"8IPfleDmNxXNIKXmX3qRQe9dT4x/h0j3vn792u/FoCLJMtOh9653hV/IPdMzMlFkwiCJicogYpMF41Oi",
"Z0AkpEJDr4/rYRLi3jstc/ja70VS8ME/xHhoF/qP0dVlq+X9DwmT3rve/3Ow3PeB/aoOfGN7NvAPMSYs",
@@ -26748,12 +27140,12 @@ var swaggerSpec = []string{
"9r7KB+R1jfCtP/C8V2QHQjDSgugZUzU4WqO4KY0O41iCWhfEexc0Iu5jsfXl3d3yW34MmYSIaojf2ams",
"vpYpEpdfiGI8AnJ4fUbmbwa//DIgHxSQEx4jzo1AGyxTg4tyHYQZ/kfjge+cOOh7Ie+OmTII46E9Z4q4",
"NiQuGvkIl+Dvc5Z4Bri6fP/h7PyYpKBpTDUlekY1uQcJJIYJ4xATwStgcCyiO5ATlkArNBUZ8AC9v8qQ",
"/phvvpWrGSTJeq+R+ZkYuQYb7E+ETM2tDT9c9snRxXGfnFzeDP/X9dXZ5U2rleJKrnjkQYqzCdEyhz6J",
"/phvvpWrGSTJeq+R+ZkYuQYb7E+ETM2tDT9c9snRxXGfnFzeDP/39dXZ5U2rleJKrnjkQYqzCdEyhz6J",
"EqHArpnQiQaJB/SGWF4DMYkSZihdzFQkOIdIKz9TVVpkIzbl1LdD/B2JpBYZ8u01wF5uw7S5YSl4uZz7",
"QPYYJwoiwWP1avPAFXFb60WQb5Y8Uxn5IlVmUEq0XvQJ41GSx4al2HNiE0M/mCJcaHuAsf9MDFdfn/CD",
"wlM2sMmSBBmXRVDkvXvqlcEjFkMdbfuEJkoQlWeGhqPA8G4qRZ75jnAukjyFJlRpSU/8VMmNRPZSkXOt",
"XpmJYwTWVUa3NqBBZcanx8xzBke5lBasJERayAXZu/7X8aslA7VyCB7QGEhCc26gsddvqBLzsal10dvI",
"W3MLsM0YoGYpDI5z95Je53v/G6QgRk5F8GF8BpLpASl6KEIlGF5sqCXEyIgsdBJOuXDwPLB6ilLfWZ9i",
"W3MLsM0YoGYpDI5z95Je53v/B6QgRk5F8GF8BpLpASl6KEIlGF5sqCXEyIgsdBJOuXDwPLB6ilLfWZ9i",
"aD8Y8DMnxfN0DNLclHl9QJRrNgcyoSzJJRiKCjGgPGc+sxhkFUnMAnLuGO/glleWX6zdi0coYp09+und",
"GNLsRkX00xrSTJOKrKII1STOZSHf4UpIBpKJuFzZtf3zUddVnWgd/O1tM81owv4AMoaJkOBMnva6sL+y",
"F5EbwBf3HC9aO83RypSgdHHFpoXpmIFE1lARoagOylGDW37IiTm/BVEJi2ANLrFrDBOaJ3pwy29QAirB",
@@ -26806,7 +27198,7 @@ var swaggerSpec = []string{
"lGQJjdBaRiYs0SDfFVt2WhOmiKPn+2rBI7O1OaOkpvHqEyWM2CkXBBI2xbQaZv9kRh13NqKq24cCPbjl",
"/+NPdwGDy8OLk6/2kYjQY+PhjQSuRIIPT1F5e5qlG1ZLzVEOWsZkS6phuvBduf1CFCQQaeW06tfF0ZTf",
"cS9FqJXlwDYg0cjnS4gYkBMUow1pxOvFeNPbnhQ5j/elGDN+28OeYxrd7ZuNU+3Y+z8/ErCGZ3IvmdbA",
"lzGNZbIAvGHLxkuxdDl2r99LgCq9P87VYp0LV9LOrITS0alyULC8xyV8mBtVA3JYBX0MCzT9/ue/b3vT",
"lzGNZbIAvGHLxkuxdDl2r99LgCq9P87VYp0LV9LOrITS0alyULC8xyV8mBtVA3JYBX0MCzT9/te/b3vT",
"LL/tfbrlKPzgiykxACq47UruZ+Z664kB0XFugDO7MC9zUDjS4JZf5IlmWQLELBjFmMPL45/id6SYrX/b",
"4/MUbnufiKNgioyFgW57Azbwkotb7vaFuMUXFjyZKvfZBpRWyGVB9fpFMF5JhEI01PofPDcWsO5A5jD0",
"FDnbMVN3fnV+zJQ/nWLBEtuZOlbTP5nRy7EqPT9tWPIFyGnY02ssKY9mrV07ZuKeB7S9OdfXweg2kWkV",
@@ -26815,26 +27207,26 @@ var swaggerSpec = []string{
"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",
@@ -26869,165 +27261,170 @@ var swaggerSpec = []string{
"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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+1 -1
View File
@@ -1,4 +1,4 @@
package cosmossdk
// Version is the SDK version, synced with Cosmos Server at build time.
const Version = "0.22.18"
const Version = "0.22.19"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.22.18",
"version": "0.22.19",
"description": "",
"main": "test-server.js",
"bugs": {
+98 -91
View File
@@ -1,40 +1,46 @@
package constellation
import (
"time"
"errors"
"strconv"
"sync"
"strings"
"crypto/tls"
"encoding/pem"
"io/ioutil"
"errors"
"fmt"
"gopkg.in/yaml.v2"
"net/url"
"io/ioutil"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/azukaar/cosmos-server/src/utils"
"github.com/azukaar/cosmos-server/src/pro"
"github.com/azukaar/cosmos-server/src/utils"
natsClient "github.com/nats-io/nats.go"
)
type NodeHeartbeat struct {
DeviceName string
IP string
IsRelay bool
DeviceName string
IP string
IsRelay bool
IsLighthouse bool
IsExitNode bool
CosmosNode int
Tunnels []utils.ProxyRouteConfig
IsExitNode bool
CosmosNode int
Tunnels []utils.ProxyRouteConfig
// RunningDeployments is the list of scheduler-managed deployment names
// currently running on this node, derived from docker containers carrying
// the `cosmos-deployment` label. Populated from docker at heartbeat time;
// see UpdateLocalTunnelCache / heartbeat goroutine in tunnels.go.
RunningDeployments []string `json:"runningDeployments"`
// RunningDeploymentVersions maps each running deployment name to the spec
// version its containers were created from (the cosmos-deployment-version
// label). The scheduler diffs this against the desired Deployment.Version to
// detect a node running a stale spec and trigger a rolling re-apply. Built
// from docker alongside RunningDeployments each heartbeat.
RunningDeploymentVersions map[string]int `json:"runningDeploymentVersions,omitempty"`
// CPUPercent and RAMPercent are the node's latest resource-usage sample,
// populated from pro.GetCurrentResources() on each heartbeat tick. Used by
// the LeastBusyPlacement strategy. Zero when MonitoringOn is false.
@@ -70,10 +76,10 @@ func sanitizeNATSUsername(username string) string {
func GetClusterIPs() ([]*url.URL, error) {
ipsMap := make(map[string]bool)
// add lighthouse IPs from nebula config
lips, _ := GetAllLighthouseIPFromTempConfig()
for _, ip := range lips {
ipsMap[ip] = true
}
@@ -141,8 +147,8 @@ func StartNATS() {
if ns != nil {
return
}
ip,err := GetCurrentDeviceIP()
ip, err := GetCurrentDeviceIP()
if err != nil {
utils.Error("[NATS] Failed to get current device IP", err)
return
@@ -151,12 +157,12 @@ func StartNATS() {
utils.Log("[NATS] Starting NATS server on " + ip + ":4222")
time.Sleep(2 * time.Second)
config := utils.GetMainConfig()
HTTPConfig := config.HTTPConfig
var tlsCert = HTTPConfig.TLSCert
var tlsKey= HTTPConfig.TLSKey
var tlsKey = HTTPConfig.TLSKey
// Ensure the PEM data is correctly formatted
certPEMBlock := []byte(tlsCert)
@@ -165,61 +171,61 @@ func StartNATS() {
// Decode PEM encoded certificate
certDERBlock, _ := pem.Decode(certPEMBlock)
if certDERBlock == nil {
utils.MajorError("[NATS] Failed to start NATS: parse certificate PEM", nil)
utils.MajorError("[NATS] Failed to start NATS: parse certificate PEM", nil)
}
// Decode PEM encoded private key
keyDERBlock, _ := pem.Decode(keyPEMBlock)
if keyDERBlock == nil {
utils.MajorError("[NATS] Failed to start NATS: parse key PEM", nil)
utils.MajorError("[NATS] Failed to start NATS: parse key PEM", nil)
}
// Create tls.Certificate using the original PEM data
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
utils.MajorError("[NATS] Failed to start NATS: create TLS certificate", err)
utils.MajorError("[NATS] Failed to start NATS: create TLS certificate", err)
}
// Configure the NATS server options
// Make users
users := []*server.User{}
for _, devices := range CachedDevices {
utils.Debug("[NATS] Adding NATS user for device: " + devices.DeviceName + " With API Key: " + devices.APIKey)
username := sanitizeNATSUsername(devices.DeviceName)
// TODO: Agent / users with less permissions
// TODO: Agent / users with less permissions
users = append(users, &server.User{
Username: username,
Password: devices.APIKey,
Permissions: &server.Permissions{
Publish: &server.SubjectPermission{
Allow: []string{
"cosmos."+username+".>", "_INBOX.>",
"cosmos._global_.>",
"_INBOX.>",
// Scheduler: leader publishes per-target deployment commands
// to cosmos.<target>.deployments.command. Scoped so non-leaders
// can't fabricate arbitrary cross-node traffic.
"cosmos.*.deployments.>",
"$KV.constellation-nodes.>",
"$KV.constellation-deployments.>",
"$JS.API.STREAM.INFO.>",
"$JS.API.>",
},
Allow: []string{
"cosmos." + username + ".>", "_INBOX.>",
"cosmos._global_.>",
"_INBOX.>",
// Scheduler: leader publishes per-target deployment commands
// to cosmos.<target>.deployments.command. Scoped so non-leaders
// can't fabricate arbitrary cross-node traffic.
"cosmos.*.deployments.>",
"$KV.constellation-nodes.>",
"$KV.constellation-deployments.>",
"$JS.API.STREAM.INFO.>",
"$JS.API.>",
},
},
Subscribe: &server.SubjectPermission{
Allow: []string{
"cosmos."+username+".>", "_INBOX.>",
"cosmos._global_.>",
"_INBOX.>",
"$KV.constellation-nodes.>",
"$KV.constellation-deployments.>",
"$JS.API.STREAM.INFO.>",
"$JS.API.>",
},
Allow: []string{
"cosmos." + username + ".>", "_INBOX.>",
"cosmos._global_.>",
"_INBOX.>",
"$KV.constellation-nodes.>",
"$KV.constellation-deployments.>",
"$JS.API.STREAM.INFO.>",
"$JS.API.>",
},
},
},
})
@@ -232,13 +238,13 @@ func StartNATS() {
}
natsHost := device.IP
natsName:= device.DeviceName
natsName := device.DeviceName
// if debug, add debug user
// if debug, add debug user
if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
users = append(users, &server.User{
Username: "DEBUG",
Password: "DEBUG",
Username: "DEBUG",
Password: "DEBUG",
Permissions: nil,
})
@@ -268,22 +274,22 @@ func StartNATS() {
ServerName: natsName,
JetStream: true,
StoreDir: utils.CONFIGFOLDER + "/jetstream",
JetStream: true,
StoreDir: utils.CONFIGFOLDER + "/jetstream",
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.NoClientCert,
Certificates: []tls.Certificate{cert},
ClientAuth: tls.NoClientCert,
InsecureSkipVerify: true,
},
Cluster: server.ClusterOpts{
Name: "Constellation",
Name: "Constellation",
Host: device.IP,
Port: 6222,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.NoClientCert,
Certificates: []tls.Certificate{cert},
ClientAuth: tls.NoClientCert,
InsecureSkipVerify: true,
},
},
@@ -312,7 +318,7 @@ func StartNATS() {
go ns.Start()
// Wait for the server to be ready
if !ns.ReadyForConnections(time.Duration(2 * (retries + 1)) * time.Second) {
if !ns.ReadyForConnections(time.Duration(2*(retries+1)) * time.Second) {
retries++
utils.Debug("[NATS] NATS server not ready...")
err = errors.New("NATS server not ready")
@@ -348,6 +354,7 @@ var clientConfigLock = sync.RWMutex{}
var NATSClientTopic = ""
var nc *nats.Conn
var js nats.JetStreamContext
func InitNATSClient() error {
if !NATSStarted {
utils.Warn("[NATS] NATS server not started, cannot initialize client")
@@ -370,12 +377,12 @@ func InitNATSClient() error {
}
utils.Log("[NATS] Connecting to NATS server...")
time.Sleep(2 * time.Second)
user, pwd, err := GetNATSCredentials()
user = sanitizeNATSUsername(user)
if err != nil {
utils.MajorError("[NATS] Error getting constellation credentials", err)
return err
@@ -392,17 +399,17 @@ func InitNATSClient() error {
// nats.DisconnectHandler(func(nc *nats.Conn) {
// utils.Log("Disconnected from NATS server - trying to reconnect")
// }),
nats.Secure(&tls.Config{
InsecureSkipVerify: true,
}),
nats.UserInfo(user, pwd),
// timeout
nats.Timeout(2*time.Second),
nats.NoEcho(),
nats.NoEcho(),
)
for err != nil {
@@ -411,15 +418,15 @@ func InitNATSClient() error {
nc = nil
return err
}
if !NebulaStarted {
utils.Error("[NATS] Nebula not started, aborting NATS client connection retry", nil)
nc = nil
return errors.New("Nebula not started, aborting NATS client connection retry")
}
clientConfigLock.Unlock()
time.Sleep(time.Duration(2 * (retries + 1)) * time.Second)
time.Sleep(time.Duration(2*(retries+1)) * time.Second)
clientConfigLock.Lock()
if !NebulaStarted {
@@ -437,10 +444,10 @@ func InitNATSClient() error {
// timeout
nats.Timeout(2*time.Second),
nats.NoEcho(),
nats.NoEcho(),
)
if err != nil {
retries++
utils.Debug("[NATS] Retrying to start NATS Client: " + err.Error())
@@ -484,26 +491,26 @@ func ClientConnectToJS() error {
if nc == nil {
return errors.New("NATS client not connected")
}
if js != nil && time.Since(lastCheck) < 5*time.Second {
return nil
}
if js != nil {
if _, err := js.AccountInfo(); err == nil {
lastCheck = time.Now()
return nil
}
}
if js != nil && time.Since(lastCheck) < 5*time.Second {
return nil
}
var err error
js, err = nc.JetStream(nats.MaxWait(6 * time.Second))
if err != nil {
return fmt.Errorf("error getting JetStream context: %w", err)
}
lastCheck = time.Now()
return nil
if js != nil {
if _, err := js.AccountInfo(); err == nil {
lastCheck = time.Now()
return nil
}
}
var err error
js, err = nc.JetStream(nats.MaxWait(6 * time.Second))
if err != nil {
return fmt.Errorf("error getting JetStream context: %w", err)
}
lastCheck = time.Now()
return nil
}
func IsClientConnected() bool {
@@ -707,4 +714,4 @@ func PingNATSClient() bool {
}
return false
}
}
+12
View File
@@ -13,3 +13,15 @@ func DeploymentsRoute(w http.ResponseWriter, req *http.Request) {
func DeploymentsIdRoute(w http.ResponseWriter, req *http.Request) {
pro.DeploymentsIdRoute(w, req, &clientConfigLock, js)
}
func DeploymentsHealthRoute(w http.ResponseWriter, req *http.Request) {
pro.DeploymentsHealthRoute(w, req, &clientConfigLock, js)
}
func DeploymentsUnbrokeRoute(w http.ResponseWriter, req *http.Request) {
pro.DeploymentsUnbrokeRoute(w, req, &clientConfigLock, js)
}
func NodesUnbrokeRoute(w http.ResponseWriter, req *http.Request) {
pro.NodesUnbrokeRoute(w, req, &clientConfigLock, js)
}
+5
View File
@@ -84,10 +84,15 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
}
}
// Leader is best-effort cluster metadata: "" when there is no cluster /
// NATS isn't up / no leader elected yet. Never blocks the device list.
leader := GetCurrentLeaderName()
// Respond with the list of devices
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": devices,
"currentDeviceName": n,
"leader": leader,
})
}
+4 -11
View File
@@ -3,7 +3,6 @@ package constellation
import (
"net/http"
"encoding/json"
"strings"
"github.com/nats-io/nats.go"
@@ -34,22 +33,16 @@ type PublicDeviceInfo struct {
// @Failure 500 {object} utils.HTTPErrorResult
// @Router /api/constellation/public-devices [get]
func DevicePublicList(w http.ResponseWriter, req *http.Request) {
if utils.CheckPermissions(w, req, utils.PERM_LOGIN) != nil {
return
}
// Check for GET method
if req.Method != "GET" {
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
return
}
// Get authorization header
auth := req.Header.Get("Authorization")
if auth == "" {
http.Error(w, "Unauthorized [1]", http.StatusUnauthorized)
return
}
// Remove "Bearer " from auth header
auth = strings.Replace(auth, "Bearer ", "", 1)
// Connect to the collection
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
defer closeDb()
+16
View File
@@ -72,6 +72,22 @@ func IsConstellationIP(ip string) bool {
return false
}
func GetConstellationFromIP(ip string) *utils.ConstellationDevice {
if !ConstellationConnected() {
return nil
}
// Check if the IP exists in the cached devices
for _, device := range CachedDevices {
if device.IP == ip {
deviceCopy := device
return &deviceCopy
}
}
return nil
}
func Init() {
utils.Log("Initializing Constellation module...")
+8 -8
View File
@@ -1087,7 +1087,7 @@ func GetCurrentDeviceName() (string, error) {
}
if configMap["cstln_device_name"] == nil {
return "", errors.New("Invalid new config file for resync")
return "", errors.New("Invalid new config file for resync - missing name")
}
deviceName := configMap["cstln_device_name"].(string)
@@ -1129,7 +1129,7 @@ func GetCurrentDevice() (utils.ConstellationDevice, error) {
if configMap["cstln_device_name"] != nil {
device.DeviceName = configMap["cstln_device_name"].(string)
} else {
return utils.ConstellationDevice{}, errors.New("Invalid new config file for resync")
return utils.ConstellationDevice{}, errors.New("Invalid new config file for resync - missing name")
}
if configMap["cstln_ip"] != nil {
@@ -1170,7 +1170,7 @@ func GetCurrentDevice() (utils.ConstellationDevice, error) {
func GetCurrentDeviceAPIKey() (string, error) {
device, err := GetCurrentDevice()
if err != nil {
return "", errors.New("current device not found in cache")
return "", err
}
return device.APIKey, nil
}
@@ -1178,7 +1178,7 @@ func GetCurrentDeviceAPIKey() (string, error) {
func GetCurrentDeviceIP() (string, error) {
device, err := GetCurrentDevice()
if err != nil {
return "", errors.New("current device not found in cache")
return "", err
}
return device.IP, nil
}
@@ -1186,7 +1186,7 @@ func GetCurrentDeviceIP() (string, error) {
func GetCurrentDeviceIsLoadbalancer() (bool, error) {
device, err := GetCurrentDevice()
if err != nil {
return false, errors.New("current device not found in cache")
return false, err
}
return device.IsLoadBalancer, nil
}
@@ -1194,7 +1194,7 @@ func GetCurrentDeviceIsLoadbalancer() (bool, error) {
func GetCurrentDeviceIsRelay() (bool, error) {
device, err := GetCurrentDevice()
if err != nil {
return false, errors.New("current device not found in cache")
return false, err
}
return device.IsRelay, nil
}
@@ -1202,7 +1202,7 @@ func GetCurrentDeviceIsRelay() (bool, error) {
func GetCurrentDeviceIsExitNode() (bool, error) {
device, err := GetCurrentDevice()
if err != nil {
return false, errors.New("current device not found in cache")
return false, err
}
return device.IsExitNode, nil
}
@@ -1213,7 +1213,7 @@ func GetCurrentDeviceHostname() (string, error) {
if utils.GetMainConfig().ConstellationConfig.ConstellationHostname != "" {
return utils.GetMainConfig().ConstellationConfig.ConstellationHostname, nil
}
return "", errors.New("current device not found in cache")
return "", err
}
return device.PublicHostname, nil
}
+14
View File
@@ -35,3 +35,17 @@ func StartSchedulerInConstellation() {
func StopSchedulerInConstellation() {
pro.StopScheduler()
}
// GetCurrentLeaderName returns the sanitized device name of the current
// scheduler leader, or "" when it cannot be determined (no cluster, NATS not
// connected, or no leader elected yet). Best-effort — never errors — so it is
// safe to call from request handlers regardless of cluster state. The name is
// returned exactly as stored (sanitized via sanitizeNATSUsername); callers
// match it client-side rather than reversing the sanitization.
func GetCurrentLeaderName() string {
name, ok := pro.GetLeaderName(&clientConfigLock, js)
if !ok {
return ""
}
return name
}
+39 -30
View File
@@ -1,17 +1,17 @@
package constellation
import (
"strings"
"strconv"
"time"
"sort"
"github.com/nats-io/nats.go"
"encoding/json"
"github.com/nats-io/nats.go"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/azukaar/cosmos-server/src/utils"
"github.com/azukaar/cosmos-server/src/pro"
"github.com/azukaar/cosmos-server/src/docker"
"github.com/azukaar/cosmos-server/src/pro"
"github.com/azukaar/cosmos-server/src/utils"
)
func getNATSReplicas() int {
@@ -32,7 +32,7 @@ func GetAllTunneledRoutes() []utils.ProxyRouteConfig {
utils.Error("Error getting current device IP for tunneled routes", err)
return tunnels
}
serverProtocol, _, configHostport := utils.GetServerRawAccess()
for _, route := range routesList {
@@ -69,7 +69,7 @@ func GetAllTunneledRoutes() []utils.ProxyRouteConfig {
}
route.Target = protocol + thisIp
if port != "" {
route.Target += ":" + port
}
@@ -263,23 +263,33 @@ func ClientHeartbeatInit() {
running = nil
}
// Per-deployment spec version those containers were created from,
// so the scheduler can spot a node running a stale spec. Degrades
// to nil on error like the name list above.
runningVersions, vrerr := docker.ListDeploymentVersionsRunningHere()
if vrerr != nil {
utils.Warn("[SCHED-NODE] failed to list cosmos-deployment versions for heartbeat: " + vrerr.Error())
runningVersions = nil
}
// Read the latest cached resource sample. Cheap — the
// sampler's own goroutine paid the cpu.Percent cost.
res := pro.GetCurrentResources()
heartbeat := NodeHeartbeat{
DeviceName: device.DeviceName,
IP: device.IP,
IsRelay: device.IsRelay,
IsLighthouse: device.IsLighthouse,
IsExitNode: device.IsExitNode,
CosmosNode: device.CosmosNode,
Tunnels: GetAllTunneledRoutes(),
RunningDeployments: running,
CPUPercent: res.CPUPercent,
RAMPercent: res.RAMPercent,
MonitoringOn: res.MonitoringOn,
Tags: device.Tags,
DeviceName: device.DeviceName,
IP: device.IP,
IsRelay: device.IsRelay,
IsLighthouse: device.IsLighthouse,
IsExitNode: device.IsExitNode,
CosmosNode: device.CosmosNode,
Tunnels: GetAllTunneledRoutes(),
RunningDeployments: running,
RunningDeploymentVersions: runningVersions,
CPUPercent: res.CPUPercent,
RAMPercent: res.RAMPercent,
MonitoringOn: res.MonitoringOn,
Tags: device.Tags,
}
heartbeatData, err := json.Marshal(heartbeat)
@@ -358,7 +368,7 @@ func UpdateLocalTunnelCache() {
keys, err := kv.Keys()
if err != nil {
clientConfigLock.RUnlock()
utils.Warn("[NATS] Error getting keys from Key-Value store during tunnel cache update "+ err.Error())
utils.Warn("[NATS] Error getting keys from Key-Value store during tunnel cache update " + err.Error())
return
}
@@ -427,13 +437,12 @@ func UpdateLocalTunnelCache() {
return copied
}
oldJSON, _ := json.Marshal(sortTunnelsForComparison(localTunnelCache))
newJSON, _ := json.Marshal(sortTunnelsForComparison(tunnels))
changed := !utils.JSONEquals(sortTunnelsForComparison(localTunnelCache), sortTunnelsForComparison(tunnels))
localTunnelCache = tunnels
lastCacheUpdate = time.Now()
if string(oldJSON) != string(newJSON) {
if changed {
utils.Log("[constellation] Tunnel cache changed, restarting HTTP server...")
go utils.RestartHTTPServer()
}
@@ -453,7 +462,7 @@ func GetLocalTunnelCache() []utils.ConstellationTunnel {
utils.Debug("[constellation] Failed to get current device load balancer status for tunnel cache retrieval " + err.Error())
return []utils.ConstellationTunnel{}
}
if !isLB {
return []utils.ConstellationTunnel{}
}
@@ -484,9 +493,9 @@ func ensureStickyBucket() (nats.KeyValue, error) {
return kv, nil
}
kv, err = js.CreateKeyValue(&nats.KeyValueConfig{
Bucket: "tunnel-sticky",
TTL: 120 * time.Second,
Storage: nats.MemoryStorage,
Bucket: "tunnel-sticky",
TTL: 120 * time.Second,
Storage: nats.MemoryStorage,
Replicas: getNATSReplicas(),
})
return kv, err
@@ -522,4 +531,4 @@ func SetStickyTarget(clientKey string, deviceName string) {
if err != nil {
utils.Error("[NATS] Error setting sticky target", err)
}
}
}
+10
View File
@@ -451,6 +451,16 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
utils.Log(fmt.Sprintf("Checking service %s...", serviceName))
OnLog(fmt.Sprintf("Checking service %s...\n", serviceName))
// Default the container name to the compose service key when no explicit
// container_name is given (standard docker-compose behavior). Cluster
// deployments routinely omit container_name. Without this, ContainerCreate
// auto-generates a random name but the rest of the flow (start, rollback,
// network connect) keeps using the empty container.Name, so ContainerStart
// hits /containers//start and the daemon returns "page not found".
if container.Name == "" {
container.Name = serviceName
}
// If container request a Cosmos network, create and attach it
if strings.ToLower(container.Labels["cosmos-network-name"]) == "auto" {
utils.Log(fmt.Sprintf("Forcing secure %s...", serviceName))
+116 -37
View File
@@ -2,13 +2,14 @@ package docker
import (
"fmt"
"strconv"
"sync"
"time"
doctype "github.com/docker/docker/api/types"
conttype "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
voltype "github.com/docker/docker/api/types/volume"
doctype "github.com/docker/docker/api/types"
"github.com/azukaar/cosmos-server/src/utils"
)
@@ -17,12 +18,23 @@ import (
// belong to a scheduler-managed deployment. The value is the deployment name.
const DeploymentLabel = "cosmos-deployment"
// DeploymentVersionLabel is the docker label key holding the integer spec
// version a container was created from (Deployment.Version, stringified). The
// scheduler compares the value a node reports against the desired version to
// decide whether a node is running a stale spec and needs a rolling re-apply.
const DeploymentVersionLabel = "cosmos-deployment-version"
// RemoveByDeploymentLabel discovers and tears down every container, network, and
// volume labeled cosmos-deployment=<deploymentName>. Mirrors the sequence the
// servapps delete modal uses (containers → 1s pause → networks + volumes in
// parallel). Per-resource failures are collected, not fatal — partial teardown
// is still progress.
func RemoveByDeploymentLabel(deploymentName string, OnLog func(string)) []error {
// (unless keepVolumes is set) volume labeled cosmos-deployment=<deploymentName>.
// Mirrors the sequence the servapps delete modal uses (containers → 1s pause →
// networks + volumes in parallel). Per-resource failures are collected, not
// fatal — partial teardown is still progress.
//
// keepVolumes skips the volume teardown: used by the scheduler's re-apply path
// when a deployment's spec version changed, so the new containers reattach to
// the existing volumes and persistent data survives the update. A full delete
// (ActionRemove) passes keepVolumes=false.
func RemoveByDeploymentLabel(deploymentName string, OnLog func(string), keepVolumes bool) []error {
if OnLog == nil {
OnLog = func(string) {}
}
@@ -107,36 +119,40 @@ func RemoveByDeploymentLabel(deploymentName string, OnLog func(string)) []error
}
}()
wg.Add(1)
go func() {
defer wg.Done()
if keepVolumes {
utils.Debug("[SCHED-NODE] RemoveByDeploymentLabel: keepVolumes set, leaving volumes for " + deploymentName)
} else {
wg.Add(1)
go func() {
defer wg.Done()
volumes, vlErr := DockerClient.VolumeList(DockerContext, voltype.ListOptions{Filters: labelFilter})
if vlErr != nil {
utils.Error("[SCHED-NODE] RemoveByDeploymentLabel: VolumeList failed for "+deploymentName, vlErr)
OnLog(utils.DoErr("Failed to list volumes for deployment %s: %s\n", deploymentName, vlErr.Error()))
errMu.Lock()
errs = append(errs, vlErr)
errMu.Unlock()
return
}
for _, v := range volumes.Volumes {
if v == nil {
continue
}
if rmErr := DockerClient.VolumeRemove(DockerContext, v.Name, true); rmErr != nil {
utils.Warn("[SCHED-NODE] failed to remove volume " + v.Name + ": " + rmErr.Error())
OnLog(utils.DoWarn("Failed to remove volume %s: %s\n", v.Name, rmErr.Error()))
volumes, vlErr := DockerClient.VolumeList(DockerContext, voltype.ListOptions{Filters: labelFilter})
if vlErr != nil {
utils.Error("[SCHED-NODE] RemoveByDeploymentLabel: VolumeList failed for "+deploymentName, vlErr)
OnLog(utils.DoErr("Failed to list volumes for deployment %s: %s\n", deploymentName, vlErr.Error()))
errMu.Lock()
errs = append(errs, rmErr)
errs = append(errs, vlErr)
errMu.Unlock()
} else {
utils.Debug("[SCHED-NODE] removed volume " + v.Name)
OnLog(fmt.Sprintf("Removed volume %s\n", v.Name))
return
}
}
}()
for _, v := range volumes.Volumes {
if v == nil {
continue
}
if rmErr := DockerClient.VolumeRemove(DockerContext, v.Name, true); rmErr != nil {
utils.Warn("[SCHED-NODE] failed to remove volume " + v.Name + ": " + rmErr.Error())
OnLog(utils.DoWarn("Failed to remove volume %s: %s\n", v.Name, rmErr.Error()))
errMu.Lock()
errs = append(errs, rmErr)
errMu.Unlock()
} else {
utils.Debug("[SCHED-NODE] removed volume " + v.Name)
OnLog(fmt.Sprintf("Removed volume %s\n", v.Name))
}
}
}()
}
wg.Wait()
@@ -172,10 +188,17 @@ func ContainerIDsByDeploymentLabel(deploymentName string) ([]string, error) {
return ids, nil
}
// ListDeploymentNamesRunningHere returns the distinct deployment names found on
// containers labeled with DeploymentLabel on this host. Called from the heartbeat
// loop to populate NodeHeartbeat.RunningDeployments. Docker is authoritative —
// no in-memory state is consulted.
// ListDeploymentNamesRunningHere returns the distinct deployment names that have
// at least one actually-running container labeled with DeploymentLabel on this
// host. Called from the heartbeat loop to populate NodeHeartbeat.RunningDeployments.
// Docker is authoritative — no in-memory state is consulted.
//
// Only containers in the "running" state count: a container that was created but
// failed to start (state "created"), or one that has since exited/died, is NOT a
// live replica. Counting those would make the scheduler treat a broken placement
// as healthy and never re-apply it (and the deployments health tab would show it
// as up). We still list All:true so the docker-side status filter, not a default
// running-only list, is what scopes the result — keeping the intent explicit.
func ListDeploymentNamesRunningHere() ([]string, error) {
if err := Connect(); err != nil {
return nil, err
@@ -183,6 +206,7 @@ func ListDeploymentNamesRunningHere() ([]string, error) {
labelFilter := filters.NewArgs()
labelFilter.Add("label", DeploymentLabel)
labelFilter.Add("status", "running")
containers, err := DockerClient.ContainerList(DockerContext, conttype.ListOptions{
All: true,
@@ -194,6 +218,11 @@ func ListDeploymentNamesRunningHere() ([]string, error) {
seen := map[string]struct{}{}
for _, c := range containers {
// Defensive: the status filter above should already exclude non-running
// containers, but guard in case the daemon returns a broader set.
if c.State != "" && c.State != "running" {
continue
}
if name := c.Labels[DeploymentLabel]; name != "" {
seen[name] = struct{}{}
}
@@ -206,6 +235,57 @@ func ListDeploymentNamesRunningHere() ([]string, error) {
return names, nil
}
// ListDeploymentVersionsRunningHere returns, per deployment with at least one
// running container on this host, the spec version those containers were created
// from (the cosmos-deployment-version label). Called from the heartbeat loop to
// populate NodeHeartbeat.RunningDeploymentVersions so the scheduler can detect a
// node running a stale spec.
//
// A container with no version label (created before versioning, or by an older
// build) reads as 0. When a deployment's containers disagree on version — the
// transient window of a multi-service rollout — the LOWEST is reported, so the
// deployment counts as stale until every one of its containers is on the new
// version. Same running-only scoping rationale as ListDeploymentNamesRunningHere.
func ListDeploymentVersionsRunningHere() (map[string]int, error) {
if err := Connect(); err != nil {
return nil, err
}
labelFilter := filters.NewArgs()
labelFilter.Add("label", DeploymentLabel)
labelFilter.Add("status", "running")
containers, err := DockerClient.ContainerList(DockerContext, conttype.ListOptions{
All: true,
Filters: labelFilter,
})
if err != nil {
return nil, err
}
versions := map[string]int{}
for _, c := range containers {
if c.State != "" && c.State != "running" {
continue
}
name := c.Labels[DeploymentLabel]
if name == "" {
continue
}
// Missing / unparseable label → version 0.
v := 0
if raw := c.Labels[DeploymentVersionLabel]; raw != "" {
if parsed, perr := strconv.Atoi(raw); perr == nil {
v = parsed
}
}
if existing, seen := versions[name]; !seen || v < existing {
versions[name] = v
}
}
return versions, nil
}
// ContainerIsRunning returns true when the given container ID is in the "running"
// Docker state. Used by the scheduler's waitForRunning polling loop.
func ContainerIsRunning(containerID string) (bool, error) {
@@ -222,4 +302,3 @@ func ContainerIsRunning(containerID string) (bool, error) {
}
return insp.State.Running, nil
}
+3
View File
@@ -701,7 +701,10 @@ func InitServer() *mux.Router {
srapiAdmin.HandleFunc("/api/constellation/edit-device", constellation.DeviceEdit_API)
srapiAdmin.HandleFunc("/api/constellation/get-next-ip", constellation.API_GetNextIP)
srapiAdmin.HandleFunc("/api/constellation/deployments", constellation.DeploymentsRoute)
srapiAdmin.HandleFunc("/api/constellation/deployments/health", constellation.DeploymentsHealthRoute)
srapiAdmin.HandleFunc("/api/constellation/deployments/{name}", constellation.DeploymentsIdRoute)
srapiAdmin.HandleFunc("/api/constellation/deployments/{name}/unbroke", constellation.DeploymentsUnbrokeRoute)
srapiAdmin.HandleFunc("/api/constellation/nodes/{name}/unbroke", constellation.NodesUnbrokeRoute)
srapiAdmin.HandleFunc("/api/events", metrics.API_ListEvents)
+1 -1
View File
@@ -206,7 +206,7 @@ func main() {
}
// @title Cosmos Server API
// @version 0.22.18
// @version 0.22.19
// @description REST API for Cosmos Cloud server management
// @BasePath /cosmos
// @securityDefinitions.apikey BearerAuth
+75
View File
@@ -133,3 +133,78 @@ func deleteDeployment(w http.ResponseWriter, req *http.Request, lock *sync.RWMut
utils.HTTPError(w, "This feature is only available in Cosmos Pro", http.StatusForbidden, "PRO001")
return
}
// DeploymentHealth is the per-deployment cluster status returned by the health
// endpoint. `desired` is the configured replica count; `actual` is how many
// nodes currently report running it (from each node's constellation-nodes
// heartbeat); `broken` mirrors the scheduler's quarantine state.
type DeploymentHealth struct {
Desired int `json:"desired"`
Actual int `json:"actual"`
Nodes []string `json:"nodes"`
Broken bool `json:"broken"`
BrokenReason string `json:"brokenReason,omitempty"`
// Version is the desired (current) spec version. UpToDate counts how many of
// the nodes reporting this deployment are running that version. Updating is
// true while a spec change is still rolling out — i.e. at least one reporting
// node is on an older version. A deployment is only fully healthy when
// Actual == Desired AND UpToDate == Actual (Updating == false).
Version int `json:"version"`
UpToDate int `json:"upToDate"`
Updating bool `json:"updating"`
}
// DeploymentsHealthRoute godoc
// @Summary Cluster health for all deployments
// @Description Returns desired vs. actual replica placement per deployment plus
// @Description scheduler quarantine state (Pro feature). Read-only: safe to call
// @Description from any node, not just the scheduler leader.
// @Tags deployments
// @Produce json
// @Security BearerAuth
// @Success 200 {object} utils.APIResponse
// @Failure 401 {object} utils.HTTPErrorResult
// @Failure 503 {object} utils.HTTPErrorResult
// @Router /api/constellation/deployments/health [get]
func DeploymentsHealthRoute(w http.ResponseWriter, req *http.Request, lock *sync.RWMutex, js nats.JetStreamContext) {
utils.Error("This is a pro and is not currently available on your server. Please upgrade to Cosmos Pro to access this feature.", nil)
utils.HTTPError(w, "This feature is only available in Cosmos Pro", http.StatusForbidden, "PRO001")
return
}
// DeploymentsUnbrokeRoute godoc
// @Summary Clear a deployment's quarantine
// @Description Removes a deployment from the scheduler's quarantine set so the
// @Description next reconcile cycle attempts to place it again (Pro feature).
// @Tags deployments
// @Produce json
// @Security BearerAuth
// @Param name path string true "Deployment name"
// @Success 200 {object} utils.APIResponse
// @Failure 401 {object} utils.HTTPErrorResult
// @Failure 405 {object} utils.HTTPErrorResult
// @Router /api/constellation/deployments/{name}/unbroke [post]
func DeploymentsUnbrokeRoute(w http.ResponseWriter, req *http.Request, lock *sync.RWMutex, js nats.JetStreamContext) {
utils.Error("This is a pro and is not currently available on your server. Please upgrade to Cosmos Pro to access this feature.", nil)
utils.HTTPError(w, "This feature is only available in Cosmos Pro", http.StatusForbidden, "PRO001")
return
}
// NodesUnbrokeRoute godoc
// @Summary Clear a node's quarantine
// @Description Removes a node from the scheduler's quarantine set and resets its
// @Description fail streak so the next reconcile cycle is free to place work on it
// @Description again (Pro feature).
// @Tags deployments
// @Produce json
// @Security BearerAuth
// @Param name path string true "Node (device) name"
// @Success 200 {object} utils.APIResponse
// @Failure 401 {object} utils.HTTPErrorResult
// @Failure 405 {object} utils.HTTPErrorResult
// @Router /api/constellation/nodes/{name}/unbroke [post]
func NodesUnbrokeRoute(w http.ResponseWriter, req *http.Request, lock *sync.RWMutex, js nats.JetStreamContext) {
utils.Error("This is a pro and is not currently available on your server. Please upgrade to Cosmos Pro to access this feature.", nil)
utils.HTTPError(w, "This feature is only available in Cosmos Pro", http.StatusForbidden, "PRO001")
return
}
+4
View File
@@ -17,3 +17,7 @@ func StopScheduler() {
func DefaultStrategies() map[string]PlacementStrategy {
return map[string]PlacementStrategy{}
}
func GetLeaderName(lock *sync.RWMutex, js nats.JetStreamContext) (string, bool) {
return "", false
}
+8 -2
View File
@@ -49,14 +49,20 @@ func tokenMiddleware(route utils.ProxyRouteConfig) func(next http.Handler) http.
enabled := route.AuthEnabled
adminOnly := route.AdminOnly
// bypass auth if from Constellation tunnel
// bypass auth if from Constellation tunnel for manager server
// necessary when authentication is not sync'd in the constellation
if ((enabled && r.Header.Get("x-cosmos-user") != "") || !enabled) {
remoteAddr, _ := utils.SplitIP(r.RemoteAddr)
isConstIP := constellation.IsConstellationIP(remoteAddr)
isConstTokenValid := constellation.CheckConstellationToken(r) == nil
device := constellation.GetConstellationFromIP(remoteAddr)
isManager := false
if device != nil {
isManager = device.CosmosNode == 2
}
if isConstIP && isConstTokenValid {
if isConstIP && isConstTokenValid && isManager {
utils.Debug("Bypassing auth for Constellation tunnel")
r.Header.Del("x-cstln-auth")
+6 -2
View File
@@ -1,6 +1,7 @@
package user
import (
"net"
"net/http"
"github.com/azukaar/cosmos-server/src/utils"
"github.com/golang-jwt/jwt"
@@ -270,7 +271,8 @@ func logOutUser(w http.ResponseWriter, req *http.Request) {
HttpOnly: false,
}
if reqHostNoPort == "localhost" || reqHostNoPort == "0.0.0.0" {
if reqHostNoPort == "localhost" || reqHostNoPort == "0.0.0.0" || net.ParseIP(reqHostNoPort) != nil {
// Must match the host-only scope used when the cookie was set (see SendUserToken).
cookie.Domain = ""
clientCookie.Domain = ""
} else {
@@ -371,7 +373,9 @@ func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mf
utils.Log("UserLogin: Setting cookie for " + reqHostNoPort)
if reqHostNoPort == "localhost" || reqHostNoPort == "0.0.0.0" {
if reqHostNoPort == "localhost" || reqHostNoPort == "0.0.0.0" || net.ParseIP(reqHostNoPort) != nil {
// IP literals (and localhost) cannot carry a Domain attribute per RFC 6265;
// browsers reject such Set-Cookie headers, so use a host-only cookie.
cookie.Domain = ""
clientCookie.Domain = ""
} else {
+16
View File
@@ -34,6 +34,22 @@ import (
"go.mongodb.org/mongo-driver/mongo"
)
// JSONEquals reports whether two values serialize to identical JSON. Handy for
// change-detection where you want to ignore differences that don't survive a
// marshal round-trip (e.g. comparing a freshly-built struct against a cached
// one). JSON preserves slice/map-key order, so callers must normalize
// order-unstable slices (sort them) and zero out volatile fields they don't
// care about BEFORE calling. A marshal error makes the values compare unequal
// (fail-open: treat as "changed" rather than silently swallowing a real diff).
func JSONEquals(a, b interface{}) bool {
aj, errA := json.Marshal(a)
bj, errB := json.Marshal(b)
if errA != nil || errB != nil {
return false
}
return string(aj) == string(bj)
}
var ConfigLock sync.Mutex
var ConfigLockInternal sync.Mutex