Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate ipam to use the grpc service #522

Merged
merged 40 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9161f3e
Migrate ipam to use the grpc service
majst01 Apr 30, 2024
843524a
Fix integration test
majst01 May 1, 2024
f9ef2a5
Remove useless nil checks
majst01 May 3, 2024
6a7c274
Merge master
majst01 May 7, 2024
f50838f
Merge branch 'master' into ipam-as-service
majst01 May 8, 2024
f27b62c
Merge master
majst01 May 30, 2024
19c4667
Merge branch 'ipam-as-service' of https://github.com/metal-stack/meta…
majst01 May 30, 2024
e210dc7
Tidy
majst01 May 30, 2024
e6c56f2
Tidy
majst01 May 30, 2024
3dece7e
Remove unneeded func
majst01 May 30, 2024
6f7d961
Merge branch 'master' into ipam-as-service
Gerrit91 Jun 3, 2024
2b74dda
Check ipam service on startup
majst01 Jun 3, 2024
ac52f45
Version fix
majst01 Jun 4, 2024
03df0fa
Update actions
majst01 Jun 4, 2024
292005d
Merge branch 'master' of https://github.com/metal-stack/metal-api int…
majst01 Jun 4, 2024
207cfec
Updates
majst01 Jun 4, 2024
301ac24
Add ipam to health status
majst01 Jun 4, 2024
ded94b4
Merge branch 'master' of https://github.com/metal-stack/metal-api int…
majst01 Jun 11, 2024
834bd81
Merge master
majst01 Jun 11, 2024
7694a58
Pin go-ipam
majst01 Jun 11, 2024
a19cc00
Update protobuf
majst01 Jun 12, 2024
b7d7a2c
merge master
majst01 Jun 12, 2024
aafab8c
Remove duplicate checks
majst01 Jun 14, 2024
2dfd650
updates
majst01 Jun 18, 2024
d3c00bb
Finally return conflict if desired
majst01 Jun 18, 2024
05539fc
Unexport
majst01 Jun 18, 2024
328f740
Smallish
majst01 Jun 18, 2024
ca16667
Proper returncodes for alreadyexist and notfound ips
majst01 Jun 20, 2024
30e144a
Handle error in search term
majst01 Jun 24, 2024
e51c045
Fix network search
majst01 Jun 24, 2024
1fbb584
Less verbose logging during tests
majst01 Jul 1, 2024
ce51743
Remove silly test
majst01 Jul 1, 2024
c4b3731
Satisfy linter
majst01 Jul 1, 2024
6b76ec2
Even less logging during tests
majst01 Jul 1, 2024
31a474b
Better description
majst01 Jul 2, 2024
021ee86
Merge master
majst01 Jul 2, 2024
f272584
Remove rootcmds, add networkquery validation
majst01 Jul 2, 2024
038d964
Useful default
majst01 Jul 2, 2024
a8b3a46
Use revision for health report of ipam
majst01 Jul 2, 2024
b83778f
Merge branch 'master' into ipam-as-service
majst01 Jul 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
cache: false

- name: Lint
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@v6
with:
args: --build-tags integration -p bugs -p unused -D protogetter --timeout=5m

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-drafter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 changes: 23 additions & 17 deletions cmd/metal-api/internal/datastore/network.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package datastore

import (
"net/netip"
"strconv"

"github.com/metal-stack/metal-api/cmd/metal-api/internal/metal"
"github.com/metal-stack/metal-api/cmd/metal-api/internal/utils"
r "gopkg.in/rethinkdb/rethinkdb-go.v6"
)

Expand Down Expand Up @@ -91,39 +91,45 @@ func (p *NetworkSearchQuery) generateTerm(rs *RethinkStore) *r.Term {
}

for _, prefix := range p.Prefixes {
ip, length := utils.SplitCIDR(prefix)
pfx, err := netip.ParsePrefix(prefix)
if err != nil {
continue
vknabel marked this conversation as resolved.
Show resolved Hide resolved
}
ip := pfx.Addr()
length := pfx.Bits()

q = q.Filter(func(row r.Term) r.Term {
return row.Field("prefixes").Map(func(p r.Term) r.Term {
return p.Field("ip")
}).Contains(r.Expr(ip))
})

if length != nil {
q = q.Filter(func(row r.Term) r.Term {
return row.Field("prefixes").Map(func(p r.Term) r.Term {
return p.Field("length")
}).Contains(r.Expr(strconv.Itoa(*length)))
})
}
q = q.Filter(func(row r.Term) r.Term {
return row.Field("prefixes").Map(func(p r.Term) r.Term {
return p.Field("length")
}).Contains(r.Expr(strconv.Itoa(length)))
})
}

for _, destPrefix := range p.DestinationPrefixes {
ip, length := utils.SplitCIDR(destPrefix)
pfx, err := netip.ParsePrefix(destPrefix)
if err != nil {
continue
}
ip := pfx.Addr()
length := pfx.Bits()

q = q.Filter(func(row r.Term) r.Term {
return row.Field("destinationprefixes").Map(func(dp r.Term) r.Term {
return dp.Field("ip")
}).Contains(r.Expr(ip))
})

if length != nil {
q = q.Filter(func(row r.Term) r.Term {
return row.Field("destinationprefixes").Map(func(dp r.Term) r.Term {
return dp.Field("length")
}).Contains(r.Expr(strconv.Itoa(*length)))
})
}
q = q.Filter(func(row r.Term) r.Term {
return row.Field("destinationprefixes").Map(func(dp r.Term) r.Term {
return dp.Field("length")
}).Contains(r.Expr(strconv.Itoa(length)))
})
}

return &q
Expand Down
165 changes: 97 additions & 68 deletions cmd/metal-api/internal/ipam/ipam.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,59 @@
package ipam

import (
"context"
"fmt"
"net/netip"

"github.com/metal-stack/metal-api/cmd/metal-api/internal/metal"
"github.com/metal-stack/metal-lib/rest"

ipam "github.com/metal-stack/go-ipam"
"connectrpc.com/connect"
goipam "github.com/metal-stack/go-ipam"
apiv1 "github.com/metal-stack/go-ipam/api/v1"

"github.com/metal-stack/go-ipam/api/v1/apiv1connect"
)

type Ipam struct {
ip ipam.Ipamer
// A IPAMer is responsible to allocate a IP for a given purpose
// On the other hand it should release the IP.
// Later Implementations should also allocate and release Networks.
type IPAMer interface {
majst01 marked this conversation as resolved.
Show resolved Hide resolved
AllocateIP(ctx context.Context, prefix metal.Prefix) (string, error)
AllocateSpecificIP(ctx context.Context, prefix metal.Prefix, specificIP string) (string, error)
ReleaseIP(ctx context.Context, ip metal.IP) error
AllocateChildPrefix(ctx context.Context, parentPrefix metal.Prefix, childLength uint8) (*metal.Prefix, error)
ReleaseChildPrefix(ctx context.Context, childPrefix metal.Prefix) error
CreatePrefix(ctx context.Context, prefix metal.Prefix) error
DeletePrefix(ctx context.Context, prefix metal.Prefix) error
PrefixUsage(ctx context.Context, cidr string) (*metal.NetworkUsage, error)
PrefixesOverlapping(existingPrefixes metal.Prefixes, newPrefixes metal.Prefixes) error
// Required for healthcheck
Check(ctx context.Context) (rest.HealthResult, error)
ServiceName() string
}

type ipam struct {
ip apiv1connect.IpamServiceClient
}

// New creates a new IPAM module.
func New(ip ipam.Ipamer) *Ipam {
return &Ipam{
func New(ip apiv1connect.IpamServiceClient) IPAMer {
return &ipam{
ip: ip,
}
}

// AllocateChildPrefix creates a child prefix from a parent prefix in the IPAM.
func (i *Ipam) AllocateChildPrefix(parentPrefix metal.Prefix, childLength uint8) (*metal.Prefix, error) {
ipamParentPrefix := i.ip.PrefixFrom(parentPrefix.String())

if ipamParentPrefix == nil {
return nil, fmt.Errorf("error finding parent prefix in ipam: %s", parentPrefix.String())
}

ipamPrefix, err := i.ip.AcquireChildPrefix(ipamParentPrefix.Cidr, childLength)
func (i *ipam) AllocateChildPrefix(ctx context.Context, parentPrefix metal.Prefix, childLength uint8) (*metal.Prefix, error) {
ipamPrefix, err := i.ip.AcquireChildPrefix(ctx, connect.NewRequest(&apiv1.AcquireChildPrefixRequest{
Cidr: parentPrefix.String(),
Length: uint32(childLength),
}))
if err != nil {
return nil, fmt.Errorf("error creating new prefix in ipam: %w", err)
}

prefix, err := metal.NewPrefixFromCIDR(ipamPrefix.Cidr)
prefix, err := metal.NewPrefixFromCIDR(ipamPrefix.Msg.Prefix.Cidr)
if err != nil {
return nil, fmt.Errorf("error creating prefix from ipam prefix: %w", err)
}
Expand All @@ -42,21 +62,10 @@ func (i *Ipam) AllocateChildPrefix(parentPrefix metal.Prefix, childLength uint8)
}

// ReleaseChildPrefix release a child prefix from a parent prefix in the IPAM.
func (i *Ipam) ReleaseChildPrefix(childPrefix metal.Prefix) error {
_, err := netip.ParsePrefix(childPrefix.String())
if err != nil {
return fmt.Errorf("invalid child prefix: %w", err)
}

ipamChildPrefix := i.ip.PrefixFrom(childPrefix.String())
if ipamChildPrefix == nil {
// FIXME: unfortunately, go-ipam does not return a proper error here so we cannot deduce if the prefix
// was already deleted or not, so if the database is down or something we continue with network deletion
// even though there could be remainings in the go-ipam db
return nil
}

err = i.ip.ReleaseChildPrefix(ipamChildPrefix)
func (i *ipam) ReleaseChildPrefix(ctx context.Context, childPrefix metal.Prefix) error {
_, err := i.ip.ReleaseChildPrefix(ctx, connect.NewRequest(&apiv1.ReleaseChildPrefixRequest{
Cidr: childPrefix.String(),
}))
if err != nil {
return fmt.Errorf("error releasing child prefix in ipam: %w", err)
}
Expand All @@ -65,90 +74,110 @@ func (i *Ipam) ReleaseChildPrefix(childPrefix metal.Prefix) error {
}

// CreatePrefix creates a prefix in the IPAM.
func (i *Ipam) CreatePrefix(prefix metal.Prefix) error {
_, err := i.ip.NewPrefix(prefix.String())
func (i *ipam) CreatePrefix(ctx context.Context, prefix metal.Prefix) error {
_, err := i.ip.CreatePrefix(ctx, connect.NewRequest(&apiv1.CreatePrefixRequest{
Cidr: prefix.String(),
}))
if err != nil {
return fmt.Errorf("unable to create prefix in ipam: %w", err)
}
return nil
}

// DeletePrefix remove a prefix in the IPAM.
func (i *Ipam) DeletePrefix(prefix metal.Prefix) error {
_, err := i.ip.DeletePrefix(prefix.String())
func (i *ipam) DeletePrefix(ctx context.Context, prefix metal.Prefix) error {
_, err := i.ip.DeletePrefix(ctx, connect.NewRequest(&apiv1.DeletePrefixRequest{
Cidr: prefix.String(),
}))
if err != nil {
return err
}
return nil
}

// AllocateIP an ip in the IPAM and returns the allocated IP as a string.
func (i *Ipam) AllocateIP(prefix metal.Prefix) (string, error) {
ipamPrefix := i.ip.PrefixFrom(prefix.String())
if ipamPrefix == nil {
return "", fmt.Errorf("error finding prefix in ipam: %s", prefix.String())
}

ipamIP, err := i.ip.AcquireIP(ipamPrefix.Cidr)
func (i *ipam) AllocateIP(ctx context.Context, prefix metal.Prefix) (string, error) {
ipamIP, err := i.ip.AcquireIP(ctx, connect.NewRequest(&apiv1.AcquireIPRequest{
PrefixCidr: prefix.String(),
Ip: nil,
}))
if err != nil {
return "", fmt.Errorf("cannot allocate ip in prefix %s in ipam: %w", prefix.String(), err)
}
if ipamIP == nil {
return "", fmt.Errorf("cannot find free ip to allocate in ipam: %s", prefix.String())
}

return ipamIP.IP.String(), nil
return ipamIP.Msg.Ip.Ip, nil
}

// AllocateSpecificIP a specific ip in the IPAM and returns the allocated IP as a string.
func (i *Ipam) AllocateSpecificIP(prefix metal.Prefix, specificIP string) (string, error) {
ipamPrefix := i.ip.PrefixFrom(prefix.String())
if ipamPrefix == nil {
return "", fmt.Errorf("error finding prefix in ipam: %s", prefix.String())
}
ipamIP, err := i.ip.AcquireSpecificIP(ipamPrefix.Cidr, specificIP)
func (i *ipam) AllocateSpecificIP(ctx context.Context, prefix metal.Prefix, specificIP string) (string, error) {
ipamIP, err := i.ip.AcquireIP(ctx, connect.NewRequest(&apiv1.AcquireIPRequest{
PrefixCidr: prefix.String(),
Ip: &specificIP,
}))
if err != nil {
return "", fmt.Errorf("cannot allocate ip in prefix %s in ipam: %w", prefix.String(), err)
}
if ipamIP == nil {
return "", fmt.Errorf("cannot find free ip to allocate in ipam: %s", prefix.String())
}

return ipamIP.IP.String(), nil
return ipamIP.Msg.Ip.Ip, nil
}

// ReleaseIP an ip in the IPAM.
func (i *Ipam) ReleaseIP(ip metal.IP) error {
ipamPrefix := i.ip.PrefixFrom(ip.ParentPrefixCidr)
if ipamPrefix == nil {
return fmt.Errorf("error finding parent prefix %s of ip %s in ipam", ip.ParentPrefixCidr, ip.IPAddress)
}

err := i.ip.ReleaseIPFromPrefix(ipamPrefix.Cidr, ip.IPAddress)
func (i *ipam) ReleaseIP(ctx context.Context, ip metal.IP) error {
_, err := i.ip.ReleaseIP(ctx, connect.NewRequest(&apiv1.ReleaseIPRequest{
PrefixCidr: ip.ParentPrefixCidr,
Ip: ip.IPAddress,
}))
if err != nil {
return fmt.Errorf("error release ip %s in prefix %s: %w", ip.IPAddress, ip.ParentPrefixCidr, err)
}
return nil
}

// PrefixUsage calculates the IP and Prefix Usage
func (i *Ipam) PrefixUsage(cidr string) (*metal.NetworkUsage, error) {
prefix := i.ip.PrefixFrom(cidr)
if prefix == nil {
return nil, fmt.Errorf("prefix for cidr:%s not found", cidr)
func (i *ipam) PrefixUsage(ctx context.Context, cidr string) (*metal.NetworkUsage, error) {
usage, err := i.ip.PrefixUsage(ctx, connect.NewRequest(&apiv1.PrefixUsageRequest{
Cidr: cidr,
}))
if err != nil {
return nil, fmt.Errorf("prefix usage for cidr:%s not found %w", cidr, err)
}
usage := prefix.Usage()

return &metal.NetworkUsage{
AvailableIPs: usage.AvailableIPs,
UsedIPs: usage.AcquiredIPs,
// FIXME add usage.AvailablePrefixList as already done here https://github.com/metal-stack/metal-api/pull/152/files#diff-fe05f7f1480be933b5c482b74af28c8b9ca7ef2591f8341eb6e6663cbaeda7baR828
AvailablePrefixes: usage.AvailableSmallestPrefixes,
UsedPrefixes: usage.AcquiredPrefixes,
AvailableIPs: usage.Msg.AvailableIps,
UsedIPs: usage.Msg.AcquiredIps,
// FIXME add usage.AvailablePrefixList as already done here
// https://github.com/metal-stack/metal-api/pull/152/files#diff-fe05f7f1480be933b5c482b74af28c8b9ca7ef2591f8341eb6e6663cbaeda7baR828
AvailablePrefixes: usage.Msg.AvailableSmallestPrefixes,
UsedPrefixes: usage.Msg.AcquiredPrefixes,
}, nil
}

// PrefixesOverlapping returns an error if prefixes overlap.
func (i *Ipam) PrefixesOverlapping(existingPrefixes metal.Prefixes, newPrefixes metal.Prefixes) error {
return i.ip.PrefixesOverlapping(existingPrefixes.String(), newPrefixes.String())
func (i *ipam) PrefixesOverlapping(existingPrefixes metal.Prefixes, newPrefixes metal.Prefixes) error {
return goipam.PrefixesOverlapping(existingPrefixes.String(), newPrefixes.String())
}

func (i *ipam) ServiceName() string {
return "ipam"
}

func (i *ipam) Check(ctx context.Context) (rest.HealthResult, error) {
resp, err := i.ip.Version(ctx, connect.NewRequest(&apiv1.VersionRequest{}))

if err != nil {
return rest.HealthResult{
Status: rest.HealthStatusUnhealthy,
}, err
}

return rest.HealthResult{
Status: rest.HealthStatusHealthy,
Message: fmt.Sprintf("connected to ipam service version:%q", resp.Msg.Version),
}, nil
}
26 changes: 23 additions & 3 deletions cmd/metal-api/internal/ipam/testing.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
package ipam

import (
"context"
"log/slog"
"net/http"
"net/http/httptest"
"testing"

goipam "github.com/metal-stack/go-ipam"
"github.com/metal-stack/go-ipam/api/v1/apiv1connect"
"github.com/metal-stack/go-ipam/pkg/service"
)

func InitTestIpam(t *testing.T) *Ipam {
ipamInstance := goipam.New()
ipamer := New(ipamInstance)
func InitTestIpam(t *testing.T) IPAMer {

ctx := context.Background()
mux := http.NewServeMux()
mux.Handle(apiv1connect.NewIpamServiceHandler(
service.New(slog.Default(), goipam.New(ctx)),
))
server := httptest.NewUnstartedServer(mux)
server.EnableHTTP2 = true
server.StartTLS()

ipamclient := apiv1connect.NewIpamServiceClient(
server.Client(),
server.URL,
)

ipamer := New(ipamclient)
return ipamer
}
20 changes: 0 additions & 20 deletions cmd/metal-api/internal/ipam/wrapper.go

This file was deleted.

Loading
Loading