work in progress. working on pod templatest.
This commit is contained in:
parent
8c5a099082
commit
207043d38f
@ -81,9 +81,7 @@ communications:
|
||||
|
||||
|
||||
Handling of capabilities:
|
||||
1. capabilities at namespace level apply to each individual pod in the
|
||||
namespace
|
||||
2. a capability is a list of templates.
|
||||
1. capabilities at namespace level is defined a template that gets the namespace name.
|
||||
|
||||
Ingress template
|
||||
|
||||
|
@ -1,12 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/goccy/go-yaml"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
)
|
||||
|
||||
var PREDEFINED_APPS = []string{"apiserver"}
|
||||
|
||||
func validateCIDR(cidr string) error {
|
||||
_, _, err := net.ParseCIDR(cidr)
|
||||
return err
|
||||
@ -36,21 +41,24 @@ func (c CIDR) MarshalYAML() ([]byte, error) {
|
||||
|
||||
// Network represents each network entry in the YAML
|
||||
type Network struct {
|
||||
Name string `yaml:"name"`
|
||||
CIDR CIDR `yaml:"cidr"`
|
||||
Except []CIDR `yaml:"except,omitempty"`
|
||||
Name string `yaml:"name"`
|
||||
CIDR CIDR `yaml:"cidr"`
|
||||
Except []CIDR `yaml:"except,omitempty"`
|
||||
Ports []string `yaml:"ports,omitempty"`
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
Name string `yaml:"name"`
|
||||
Ports []string `yaml:"ports,omitempty"`
|
||||
MatchLabels map[string]string `yaml:"matchLabels"`
|
||||
Namespace string `yaml:"-"`
|
||||
}
|
||||
|
||||
type Namespace struct {
|
||||
Name string `yaml:"name"`
|
||||
Capabilities []string `yaml:"capabilities"`
|
||||
Applications []Application `yaml:"applications"`
|
||||
Name string `yaml:"name"`
|
||||
Open bool `yaml:"open"`
|
||||
Capabilities []string `yaml:"capabilities"`
|
||||
Applications []*Application `yaml:"applications"`
|
||||
}
|
||||
|
||||
type Communication struct {
|
||||
@ -61,9 +69,9 @@ type Communication struct {
|
||||
|
||||
// Config represents the top-level YAML structure
|
||||
type Config struct {
|
||||
Networks []Network `yaml:"networks,omitempty"`
|
||||
Namespaces []Namespace `yaml:"namespaces,omitempty"`
|
||||
Communications []Communication `yaml:"communications,omitempty"`
|
||||
Networks []*Network `yaml:"networks,omitempty"`
|
||||
Namespaces []*Namespace `yaml:"namespaces,omitempty"`
|
||||
Communications []*Communication `yaml:"communications,omitempty"`
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
@ -88,8 +96,15 @@ func (c Config) Validate() error {
|
||||
networks[network.Name] = true
|
||||
}
|
||||
|
||||
// application names must be unique
|
||||
apps := make(map[string]bool)
|
||||
// application names must be unique and may not conflict with predefined applications
|
||||
apps := map[string]bool{
|
||||
"apiserver": true,
|
||||
}
|
||||
// application names may also not conflict with network names.
|
||||
for _, network := range c.Networks {
|
||||
apps[network.Name] = true
|
||||
}
|
||||
|
||||
for _, namespace := range c.Namespaces {
|
||||
for _, app := range namespace.Applications {
|
||||
if apps[app.Name] {
|
||||
@ -115,3 +130,54 @@ func (c Config) Validate() error {
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (c Config) GetApplication(name string) (*Application, *Network, string) {
|
||||
if slices.Contains(PREDEFINED_APPS, name) {
|
||||
return nil, nil, name
|
||||
}
|
||||
for _, network := range c.Networks {
|
||||
if name == network.Name {
|
||||
return nil, network, ""
|
||||
}
|
||||
}
|
||||
for _, ns := range c.Namespaces {
|
||||
for _, app := range ns.Applications {
|
||||
if app.Name == name {
|
||||
return app, nil, ""
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil, ""
|
||||
}
|
||||
|
||||
func LoadConfig(file string) (*Config, error) {
|
||||
fmt.Fprintf(os.Stderr, "Reading config %s\n", file)
|
||||
yamlFile, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Parse the YAML content
|
||||
dec := yaml.NewDecoder(bytes.NewReader(yamlFile),
|
||||
yaml.UseJSONUnmarshaler(),
|
||||
yaml.DisallowUnknownField(),
|
||||
)
|
||||
var config Config
|
||||
err = dec.Decode(&config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing YAML: %v", err)
|
||||
}
|
||||
err = config.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// every application must have its namespace field set
|
||||
for _, ns := range config.Namespaces {
|
||||
for _, app := range ns.Applications {
|
||||
app.Namespace = ns.Name
|
||||
}
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
115
cmd/policygen/generator.go
Normal file
115
cmd/policygen/generator.go
Normal file
@ -0,0 +1,115 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Generator interface {
|
||||
GenerateNamespace(writer io.Writer, namespace *Namespace) error
|
||||
GenerateCommunicationRule(writer io.Writer, app *Application, ingress *Ingress, egress *Egress) error
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
Applications []*Application
|
||||
Networks []*Network
|
||||
Predefined []string
|
||||
}
|
||||
|
||||
func (p *Peer) append(app *Application, network *Network, predefined string) {
|
||||
if app != nil {
|
||||
p.Applications = append(p.Applications, app)
|
||||
}
|
||||
if network != nil {
|
||||
p.Networks = append(p.Networks, network)
|
||||
}
|
||||
if predefined != "" {
|
||||
p.Predefined = append(p.Predefined, predefined)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Peer) Empty() bool {
|
||||
return len(p.Applications)+len(p.Networks)+len(p.Predefined) == 0
|
||||
}
|
||||
|
||||
func (p Peer) String() string {
|
||||
res := ""
|
||||
for _, app := range p.Applications {
|
||||
res += "app:" + app.Name + " "
|
||||
}
|
||||
for _, net := range p.Networks {
|
||||
res += "net:" + net.Name + " "
|
||||
}
|
||||
for _, pre := range p.Predefined {
|
||||
res += "pre:" + pre + " "
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type Ingress struct {
|
||||
Peer
|
||||
}
|
||||
type Egress struct {
|
||||
Peer
|
||||
}
|
||||
|
||||
func Generate(writer io.Writer, generator Generator, config *Config) error {
|
||||
|
||||
log.Printf("CONFIG %+v", config)
|
||||
for _, ns := range config.Namespaces {
|
||||
err := generator.GenerateNamespace(os.Stdout, ns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Loop over all applications and gather the ingress and egress for each application
|
||||
var applications = make(map[string]*Application)
|
||||
var ingresses = make(map[string]*Ingress)
|
||||
var egresses = make(map[string]*Egress)
|
||||
for _, ns := range config.Namespaces {
|
||||
for _, app := range ns.Applications {
|
||||
applications[app.Name] = app
|
||||
if ingresses[app.Name] == nil {
|
||||
ingresses[app.Name] = &Ingress{}
|
||||
}
|
||||
if egresses[app.Name] == nil {
|
||||
egresses[app.Name] = &Egress{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, communication := range config.Communications {
|
||||
for _, from := range communication.From {
|
||||
appFrom, networkFrom, predefinedFrom := config.GetApplication(from)
|
||||
for _, to := range communication.To {
|
||||
appTo, networkTo, predefinedTo := config.GetApplication(to)
|
||||
if appFrom != nil {
|
||||
// we have an egress
|
||||
egress := egresses[from]
|
||||
egress.append(appTo, networkTo, predefinedTo)
|
||||
}
|
||||
if appTo != nil {
|
||||
// we have an ingress
|
||||
ingress := ingresses[to]
|
||||
ingress.append(appFrom, networkFrom, predefinedFrom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loop over all apps and configure them
|
||||
for app, ingress := range ingresses {
|
||||
egress := egresses[app]
|
||||
if !ingress.Empty() || !egress.Empty() {
|
||||
fmt.Fprintf(os.Stderr, "RULE %s\n", app)
|
||||
fmt.Fprintf(os.Stderr, " IN %s\n", ingress)
|
||||
fmt.Fprintf(os.Stderr, " OUT %s\n", egress)
|
||||
generator.GenerateCommunicationRule(writer, applications[app], ingress, egress)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/spf13/cobra"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
@ -18,32 +18,39 @@ func execute(files []string, options *Options) error {
|
||||
return fmt.Errorf("File expected")
|
||||
}
|
||||
for _, file := range files {
|
||||
fmt.Fprintf(os.Stderr, "Reading config %s\n", file)
|
||||
yamlFile, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Parse the YAML content
|
||||
dec := yaml.NewDecoder(bytes.NewReader(yamlFile),
|
||||
yaml.UseJSONUnmarshaler(),
|
||||
yaml.DisallowUnknownField(),
|
||||
)
|
||||
var config Config
|
||||
err = dec.Decode(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error parsing YAML: %v", err)
|
||||
}
|
||||
err = config.Validate()
|
||||
config, err := LoadConfig(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("PARSED %+v\n", config)
|
||||
|
||||
policyTemplates, err := NewPolicyTemplates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var generator Generator
|
||||
generator = NetworkPolicyGenerrator{
|
||||
config: config,
|
||||
policyTemplates: policyTemplates,
|
||||
}
|
||||
Generate(os.Stdout, generator, config)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
val := map[string]string{
|
||||
"abc": "1",
|
||||
}
|
||||
data, err := yaml.Marshal(val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("val %s", string(data))
|
||||
//os.Exit(1)
|
||||
|
||||
options := Options{
|
||||
cni: "cilium",
|
||||
policyType: "netpol",
|
||||
|
61
cmd/policygen/netpol_generator.go
Normal file
61
cmd/policygen/netpol_generator.go
Normal file
@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type NetworkPolicyGenerrator struct {
|
||||
config *Config
|
||||
policyTemplates *PolicyTemplates
|
||||
}
|
||||
|
||||
func (g NetworkPolicyGenerrator) GenerateNamespace(writer io.Writer, namespace *Namespace) error {
|
||||
fmt.Fprintf(os.Stderr, "Namespace %s\n", namespace.Name)
|
||||
|
||||
templates := g.policyTemplates.NamespaceTemplates("netpol", namespace.Capabilities)
|
||||
log.Printf("Got %d templates", len(templates))
|
||||
|
||||
for _, template := range templates {
|
||||
err := template.Execute(writer, &namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error using template %s: %w", template.Name(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g NetworkPolicyGenerrator) GenerateCommunicationRule(
|
||||
writer io.Writer,
|
||||
app *Application,
|
||||
ingress *Ingress,
|
||||
egress *Egress) error {
|
||||
|
||||
if len(ingress.Applications)+
|
||||
len(ingress.Networks)+
|
||||
len(egress.Applications)+
|
||||
len(egress.Networks) > 0 {
|
||||
// non-trivial regular network policy
|
||||
|
||||
tmpl := g.policyTemplates.ApplicationTemplate("netpol")
|
||||
log.Printf("Found template %v for pod %s", tmpl, app.Name)
|
||||
if tmpl != nil {
|
||||
|
||||
log.Printf("EXECUTING %s", app.Namespace)
|
||||
err := tmpl.Execute(writer, map[string]any{
|
||||
"app": app,
|
||||
"ingress": ingress,
|
||||
"egress": egress,
|
||||
"labels": map[string]string{
|
||||
"policy-generator": "1",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -2,10 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
import "embed"
|
||||
import sprig "github.com/Masterminds/sprig/v3" // This provides most Helm functions
|
||||
@ -66,20 +66,24 @@ func showContents(files fs.FS) {
|
||||
}
|
||||
}
|
||||
|
||||
func loadTemplates() (*template.Template, error) {
|
||||
type PolicyTemplates struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
func NewPolicyTemplates() (*PolicyTemplates, error) {
|
||||
showContents(templateFS)
|
||||
|
||||
// Parse all templates at once from the embedded FS
|
||||
tmpl := NewTemplate()
|
||||
|
||||
err := loadTemplatesGlob(tmpl)
|
||||
err := loadTemplatesAll(tmpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tmpl, err
|
||||
return &PolicyTemplates{templates: tmpl}, err
|
||||
}
|
||||
|
||||
func loadTemplatesGlob(tmpl *template.Template) error {
|
||||
func loadTemplatesAll(tmpl *template.Template) error {
|
||||
return fs.WalkDir(templateFS, ".", func(path string, d os.DirEntry, err error) error {
|
||||
if strings.HasSuffix(path, ".yaml") {
|
||||
data, err := fs.ReadFile(templateFS, path)
|
||||
@ -87,8 +91,36 @@ func loadTemplatesGlob(tmpl *template.Template) error {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Loading template %s\n", path)
|
||||
tmpl.New(path).Parse(string(data))
|
||||
_, err = tmpl.New(path).Option("missingkey=error").Parse(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t *PolicyTemplates) NamespaceTemplates(policyType string, capabilities []string) []*template.Template {
|
||||
res := make([]*template.Template, 0)
|
||||
tmpl := t.templates.Lookup(fmt.Sprintf("templates/%s/namespace/namespace.yaml", policyType))
|
||||
if tmpl != nil {
|
||||
res = append(res, tmpl)
|
||||
}
|
||||
for _, capability := range capabilities {
|
||||
tmpl := t.templates.Lookup(fmt.Sprintf("templates/%s/namespace/%s.yaml", policyType, capability))
|
||||
if tmpl != nil {
|
||||
res = append(res, tmpl)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (t *PolicyTemplates) ApplicationTemplate(policyType string) *template.Template {
|
||||
tmpl := t.templates.Lookup(fmt.Sprintf("templates/%s/pod/pod.yaml", policyType))
|
||||
return tmpl
|
||||
}
|
||||
|
||||
func (t *PolicyTemplates) PredefineApplicationPolicyTemplate(policyType string, predefined string) *template.Template {
|
||||
tmpl := t.templates.Lookup(fmt.Sprintf("templates/pod/%s/%s.yaml", policyType, predefined))
|
||||
return tmpl
|
||||
}
|
||||
|
0
cmd/policygen/templates/linkerd/dummy.yaml
Normal file
0
cmd/policygen/templates/linkerd/dummy.yaml
Normal file
@ -1,15 +0,0 @@
|
||||
kind: CiliumNetworkPolicy
|
||||
apiVersion: cilium.io/v2
|
||||
metadata:
|
||||
name: {{.name}}
|
||||
namespace: {{.namespace}}
|
||||
spec:
|
||||
endpointSelector:
|
||||
{{ .selector }}
|
||||
egress:
|
||||
- toEntities:
|
||||
- kube-apiserver
|
||||
- toPorts:
|
||||
- ports:
|
||||
- port: "6443"
|
||||
protocol: TCP
|
@ -1,14 +0,0 @@
|
||||
kind: NetworkPolicy
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: "{{.name}}"
|
||||
namespace: "{{.namespace}}"
|
||||
spec:
|
||||
policyTypes:
|
||||
- Egress
|
||||
podSelector:
|
||||
{{.selector}}
|
||||
egress:
|
||||
{{- range $from := .from }}
|
||||
- {{ $from | nindent 4 }}
|
||||
{{- end }}
|
@ -1,14 +0,0 @@
|
||||
kind: NetworkPolicy
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: "{{.name}}"
|
||||
namespace: "{{.namespace}}"
|
||||
spec:
|
||||
policyTypes:
|
||||
- Ingress
|
||||
podSelector:
|
||||
{{.selector}}
|
||||
ingress:
|
||||
{{- range $from := .from }}
|
||||
- {{ $from | nindent 4 }}
|
||||
{{- end }}
|
6
cmd/policygen/templates/netpol/namespace/linkerd.yaml
Normal file
6
cmd/policygen/templates/netpol/namespace/linkerd.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
####################################################################################
|
||||
# LINKERD NETPOL TBD
|
||||
####################################################################################
|
||||
|
||||
|
14
cmd/policygen/templates/netpol/namespace/namespace.yaml
Normal file
14
cmd/policygen/templates/netpol/namespace/namespace.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
{{- if not .Open }}
|
||||
---
|
||||
kind: NetworkPolicy
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: default-allow-nothing
|
||||
namespace: "{{.Name}}"
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
{{ end }}
|
@ -3,11 +3,24 @@ apiVersion: cilium.io/v2
|
||||
metadata:
|
||||
name: {{.name}}
|
||||
namespace: {{.namespace}}
|
||||
labels: "{{ .labels | toYaml | nindent 4 }}"
|
||||
spec:
|
||||
endpointSelector:
|
||||
{{ .selector }}
|
||||
{{- if .from }}
|
||||
ingress:
|
||||
- fromEntities:
|
||||
- kube-apiserver
|
||||
# See https://github.com/cilium/cilium/issues/35401
|
||||
- remote-node
|
||||
{{- end }}
|
||||
{{- if .to }}
|
||||
egress:
|
||||
- toEntities:
|
||||
- kube-apiserver
|
||||
- toPorts:
|
||||
- ports:
|
||||
- port: "6443"
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
|
56
cmd/policygen/templates/netpol/pod/pod.yaml
Normal file
56
cmd/policygen/templates/netpol/pod/pod.yaml
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
{{- define "peer" }}
|
||||
- podSelector:
|
||||
matchLabels: {{ .MatchLabels | toYaml | nindent 12 }}
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: {{ .Namespace }}
|
||||
{{- if .Ports }}
|
||||
ports:
|
||||
# TODO: add protocol
|
||||
{{- range $port := .Ports }}
|
||||
- port: {{ $port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
-
|
||||
{{- define "ports" }}
|
||||
{{- range $port := . }}
|
||||
PORT {{ $port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
kind: NetworkPolicy
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: "{{.app.Name}}"
|
||||
namespace: "{{.app.Namespace}}"
|
||||
labels: {{ .labels | toYaml | nindent 4 }}
|
||||
spec:
|
||||
podSelector: {{ .app.MatchLabels | toYaml | nindent 4 }}
|
||||
policyTypes:
|
||||
{{- if or .ingress.Applications .ingress.Networks }}
|
||||
- Ingress
|
||||
{{- end }}
|
||||
{{- if or .egress.Applications .egress.Networks }}
|
||||
- Egress
|
||||
{{- end }}
|
||||
|
||||
{{- if or .ingress.Applications .ingress.Networks }}
|
||||
ingress:
|
||||
from:
|
||||
{{- range $ingress := .ingress.Applications }}
|
||||
{{- template "peer" $ingress }}
|
||||
{{- template "ports" $ingress.Ports }}
|
||||
{{- end }}
|
||||
{{- range $ingress := .ingress.Networks }}
|
||||
- ipBlock:
|
||||
cidr: {{ $ingress.CIDR}}
|
||||
except:
|
||||
{{- range $except := $ingress.Except }}
|
||||
- {{ $except }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
THEEND
|
@ -12,6 +12,7 @@ networks:
|
||||
|
||||
namespaces:
|
||||
- name: wamblee-org
|
||||
open: true
|
||||
capabilities:
|
||||
- linkerd
|
||||
applications:
|
||||
@ -29,22 +30,24 @@ namespaces:
|
||||
- name: httpd-wamblee-org
|
||||
matchLabels:
|
||||
app: wamblee-org
|
||||
ports:
|
||||
- 1000
|
||||
|
||||
communications:
|
||||
- from: # can we support both string and list of strings?
|
||||
- httpd-wamblee-org
|
||||
- internet
|
||||
to:
|
||||
- nexus-server
|
||||
- wamblee-static
|
||||
- wamblee-safe
|
||||
|
||||
# or limiting ports further
|
||||
- from:
|
||||
- httpd-wamblee-org
|
||||
to:
|
||||
- nexus-server
|
||||
ports:
|
||||
- 8081
|
||||
- 8082
|
||||
|
||||
|
||||
# # or limiting ports further
|
||||
# - from:
|
||||
# - httpd-wamblee-org
|
||||
# to:
|
||||
# - nexus-server
|
||||
# ports:
|
||||
# - 8081
|
||||
# - 8082
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user