mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
bump knftables to v0.0.17
This commit is contained in:
parent
233bc735b5
commit
4effb05741
2
go.mod
2
go.mod
@ -117,7 +117,7 @@ require (
|
|||||||
k8s.io/sample-apiserver v0.0.0
|
k8s.io/sample-apiserver v0.0.0
|
||||||
k8s.io/system-validators v1.8.0
|
k8s.io/system-validators v1.8.0
|
||||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||||
sigs.k8s.io/knftables v0.0.16
|
sigs.k8s.io/knftables v0.0.17
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
|
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
|
||||||
sigs.k8s.io/yaml v1.4.0
|
sigs.k8s.io/yaml v1.4.0
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@ -970,8 +970,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsA
|
|||||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||||
sigs.k8s.io/knftables v0.0.16 h1:ZpTfNsjnidgoXdxxzcZLdSctqkpSO3QB3jo3zQ4PXqM=
|
sigs.k8s.io/knftables v0.0.17 h1:wGchTyRF/iGTIjd+vRaR1m676HM7jB8soFtyr/148ic=
|
||||||
sigs.k8s.io/knftables v0.0.16/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk=
|
sigs.k8s.io/knftables v0.0.17/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk=
|
||||||
sigs.k8s.io/kustomize/api v0.17.2 h1:E7/Fjk7V5fboiuijoZHgs4aHuexi5Y2loXlVOAVAG5g=
|
sigs.k8s.io/kustomize/api v0.17.2 h1:E7/Fjk7V5fboiuijoZHgs4aHuexi5Y2loXlVOAVAG5g=
|
||||||
sigs.k8s.io/kustomize/api v0.17.2/go.mod h1:UWTz9Ct+MvoeQsHcJ5e+vziRRkwimm3HytpZgIYqye0=
|
sigs.k8s.io/kustomize/api v0.17.2/go.mod h1:UWTz9Ct+MvoeQsHcJ5e+vziRRkwimm3HytpZgIYqye0=
|
||||||
sigs.k8s.io/kustomize/cmd/config v0.14.1/go.mod h1:Sw1cPsFqh4uYczCWKlidPgMrsffLPCAB+7ytYLlauY4=
|
sigs.k8s.io/kustomize/cmd/config v0.14.1/go.mod h1:Sw1cPsFqh4uYczCWKlidPgMrsffLPCAB+7ytYLlauY4=
|
||||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -1231,7 +1231,7 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client
|
|||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
sigs.k8s.io/json
|
sigs.k8s.io/json
|
||||||
sigs.k8s.io/json/internal/golang/encoding/json
|
sigs.k8s.io/json/internal/golang/encoding/json
|
||||||
# sigs.k8s.io/knftables v0.0.16
|
# sigs.k8s.io/knftables v0.0.17
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
sigs.k8s.io/knftables
|
sigs.k8s.io/knftables
|
||||||
# sigs.k8s.io/kustomize/api v0.17.2
|
# sigs.k8s.io/kustomize/api v0.17.2
|
||||||
|
23
vendor/sigs.k8s.io/knftables/CHANGELOG.md
generated
vendored
23
vendor/sigs.k8s.io/knftables/CHANGELOG.md
generated
vendored
@ -1,5 +1,28 @@
|
|||||||
# ChangeLog
|
# ChangeLog
|
||||||
|
|
||||||
|
## v0.0.17
|
||||||
|
|
||||||
|
- `ListRules()` now accepts `""` for the chain name, meaning to list
|
||||||
|
all rules in the table. (`@caseydavenport`)
|
||||||
|
|
||||||
|
- `ListElements()` now handles elements with prefix/CIDR values (e.g.,
|
||||||
|
`"192.168.0.0/16"`; these are represented specially in the JSON
|
||||||
|
format and the old code didn't handle them). (`@caseydavenport`)
|
||||||
|
|
||||||
|
- Added `NumOperations()` to `Transaction` (which lets you figure out
|
||||||
|
belatedly whether you added anything to the transaction or not, and
|
||||||
|
could also be used for metrics). (`@fasaxc`)
|
||||||
|
|
||||||
|
- `knftables.Interface` now reuses the same `bytes.Buffer` for each
|
||||||
|
call to `nft` rather than constructing a new one each time, saving
|
||||||
|
time and memory. (`@aroradaman`)
|
||||||
|
|
||||||
|
- Fixed map element deletion in `knftables.Fake` to not mistakenly
|
||||||
|
require that you fill in the `.Value` of the element. (`@npinaeva`)
|
||||||
|
|
||||||
|
- Added `Fake.LastTransaction`, to retrieve the most-recently-executed
|
||||||
|
transaction. (`@npinaeva`)
|
||||||
|
|
||||||
## v0.0.16
|
## v0.0.16
|
||||||
|
|
||||||
- Fixed a bug in `Fake.ParseDump()` when using IPv6. (`@npinaeva`)
|
- Fixed a bug in `Fake.ParseDump()` when using IPv6. (`@npinaeva`)
|
||||||
|
6
vendor/sigs.k8s.io/knftables/README.md
generated
vendored
6
vendor/sigs.k8s.io/knftables/README.md
generated
vendored
@ -7,9 +7,11 @@ specifically focuses on supporting Kubernetes components which are
|
|||||||
using nftables in the way that nftables is supposed to be used (as
|
using nftables in the way that nftables is supposed to be used (as
|
||||||
opposed to using nftables in a naively-translated-from-iptables way,
|
opposed to using nftables in a naively-translated-from-iptables way,
|
||||||
or using nftables to do totally valid things that aren't the sorts of
|
or using nftables to do totally valid things that aren't the sorts of
|
||||||
things Kubernetes components are likely to need to do).
|
things Kubernetes components are likely to need to do; see the
|
||||||
|
"[iptables porting](./docs/iptables-porting.md)" doc for more thoughts
|
||||||
|
on porting old iptables-based components to nftables.)
|
||||||
|
|
||||||
It is still under development and is not yet API stable. (See the
|
knftables is still under development and is not yet API stable. (See the
|
||||||
section on "Possible future changes" below.)
|
section on "Possible future changes" below.)
|
||||||
|
|
||||||
The library is implemented as a wrapper around the `nft` CLI, because
|
The library is implemented as a wrapper around the `nft` CLI, because
|
||||||
|
27
vendor/sigs.k8s.io/knftables/fake.go
generated
vendored
27
vendor/sigs.k8s.io/knftables/fake.go
generated
vendored
@ -34,6 +34,10 @@ type Fake struct {
|
|||||||
// Table contains the Interface's table. This will be `nil` until you `tx.Add()`
|
// Table contains the Interface's table. This will be `nil` until you `tx.Add()`
|
||||||
// the table.
|
// the table.
|
||||||
Table *FakeTable
|
Table *FakeTable
|
||||||
|
|
||||||
|
// LastTransaction is the last transaction passed to Run(). It will remain set until the
|
||||||
|
// next time Run() is called. (It is not affected by Check().)
|
||||||
|
LastTransaction *Transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
// FakeTable wraps Table for the Fake implementation
|
// FakeTable wraps Table for the Fake implementation
|
||||||
@ -120,13 +124,23 @@ func (fake *Fake) List(_ context.Context, objectType string) ([]string, error) {
|
|||||||
// ListRules is part of Interface
|
// ListRules is part of Interface
|
||||||
func (fake *Fake) ListRules(_ context.Context, chain string) ([]*Rule, error) {
|
func (fake *Fake) ListRules(_ context.Context, chain string) ([]*Rule, error) {
|
||||||
if fake.Table == nil {
|
if fake.Table == nil {
|
||||||
return nil, notFoundError("no such chain %q", chain)
|
return nil, notFoundError("no such table %q", fake.table)
|
||||||
}
|
}
|
||||||
ch := fake.Table.Chains[chain]
|
|
||||||
if ch == nil {
|
rules := []*Rule{}
|
||||||
return nil, notFoundError("no such chain %q", chain)
|
if chain == "" {
|
||||||
|
// Include all rules across all chains.
|
||||||
|
for _, ch := range fake.Table.Chains {
|
||||||
|
rules = append(rules, ch.Rules...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ch := fake.Table.Chains[chain]
|
||||||
|
if ch == nil {
|
||||||
|
return nil, notFoundError("no such chain %q", chain)
|
||||||
|
}
|
||||||
|
rules = append(rules, ch.Rules...)
|
||||||
}
|
}
|
||||||
return ch.Rules, nil
|
return rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListElements is part of Interface
|
// ListElements is part of Interface
|
||||||
@ -155,6 +169,7 @@ func (fake *Fake) NewTransaction() *Transaction {
|
|||||||
|
|
||||||
// Run is part of Interface
|
// Run is part of Interface
|
||||||
func (fake *Fake) Run(_ context.Context, tx *Transaction) error {
|
func (fake *Fake) Run(_ context.Context, tx *Transaction) error {
|
||||||
|
fake.LastTransaction = tx
|
||||||
updatedTable, err := fake.run(tx)
|
updatedTable, err := fake.run(tx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fake.Table = updatedTable
|
fake.Table = updatedTable
|
||||||
@ -341,7 +356,7 @@ func (fake *Fake) run(tx *Transaction) (*FakeTable, error) {
|
|||||||
return nil, fmt.Errorf("unhandled operation %q", op.verb)
|
return nil, fmt.Errorf("unhandled operation %q", op.verb)
|
||||||
}
|
}
|
||||||
case *Element:
|
case *Element:
|
||||||
if len(obj.Value) == 0 {
|
if obj.Set != "" {
|
||||||
existingSet := updatedTable.Sets[obj.Set]
|
existingSet := updatedTable.Sets[obj.Set]
|
||||||
if existingSet == nil {
|
if existingSet == nil {
|
||||||
return nil, notFoundError("no such set %q", obj.Set)
|
return nil, notFoundError("no such set %q", obj.Set)
|
||||||
|
63
vendor/sigs.k8s.io/knftables/nftables.go
generated
vendored
63
vendor/sigs.k8s.io/knftables/nftables.go
generated
vendored
@ -17,11 +17,13 @@ limitations under the License.
|
|||||||
package knftables
|
package knftables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interface is an interface for running nftables commands against a given family and table.
|
// Interface is an interface for running nftables commands against a given family and table.
|
||||||
@ -43,7 +45,8 @@ type Interface interface {
|
|||||||
// list and no error.
|
// list and no error.
|
||||||
List(ctx context.Context, objectType string) ([]string, error)
|
List(ctx context.Context, objectType string) ([]string, error)
|
||||||
|
|
||||||
// ListRules returns a list of the rules in a chain, in order. Note that at the
|
// ListRules returns a list of the rules in a chain, in order. If no chain name is
|
||||||
|
// specified, then all rules within the table will be returned. Note that at the
|
||||||
// present time, the Rule objects will have their `Comment` and `Handle` fields
|
// present time, the Rule objects will have their `Comment` and `Handle` fields
|
||||||
// filled in, but *not* the actual `Rule` field. So this can only be used to find
|
// filled in, but *not* the actual `Rule` field. So this can only be used to find
|
||||||
// the handles of rules if they have unique comments to recognize them by, or if
|
// the handles of rules if they have unique comments to recognize them by, or if
|
||||||
@ -70,6 +73,9 @@ type nftContext struct {
|
|||||||
type realNFTables struct {
|
type realNFTables struct {
|
||||||
nftContext
|
nftContext
|
||||||
|
|
||||||
|
bufferMutex sync.Mutex
|
||||||
|
buffer *bytes.Buffer
|
||||||
|
|
||||||
exec execer
|
exec execer
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
@ -84,8 +90,8 @@ func newInternal(family Family, table string, execer execer) (Interface, error)
|
|||||||
family: family,
|
family: family,
|
||||||
table: table,
|
table: table,
|
||||||
},
|
},
|
||||||
|
buffer: &bytes.Buffer{},
|
||||||
exec: execer,
|
exec: execer,
|
||||||
}
|
}
|
||||||
|
|
||||||
nft.path, err = nft.exec.LookPath("nft")
|
nft.path, err = nft.exec.LookPath("nft")
|
||||||
@ -135,34 +141,42 @@ func (nft *realNFTables) NewTransaction() *Transaction {
|
|||||||
|
|
||||||
// Run is part of Interface
|
// Run is part of Interface
|
||||||
func (nft *realNFTables) Run(ctx context.Context, tx *Transaction) error {
|
func (nft *realNFTables) Run(ctx context.Context, tx *Transaction) error {
|
||||||
|
nft.bufferMutex.Lock()
|
||||||
|
defer nft.bufferMutex.Unlock()
|
||||||
|
|
||||||
if tx.err != nil {
|
if tx.err != nil {
|
||||||
return tx.err
|
return tx.err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := tx.asCommandBuf()
|
nft.buffer.Reset()
|
||||||
|
err := tx.populateCommandBuf(nft.buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, nft.path, "-f", "-")
|
cmd := exec.CommandContext(ctx, nft.path, "-f", "-")
|
||||||
cmd.Stdin = buf
|
cmd.Stdin = nft.buffer
|
||||||
_, err = nft.exec.Run(cmd)
|
_, err = nft.exec.Run(cmd)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check is part of Interface
|
// Check is part of Interface
|
||||||
func (nft *realNFTables) Check(ctx context.Context, tx *Transaction) error {
|
func (nft *realNFTables) Check(ctx context.Context, tx *Transaction) error {
|
||||||
|
nft.bufferMutex.Lock()
|
||||||
|
defer nft.bufferMutex.Unlock()
|
||||||
|
|
||||||
if tx.err != nil {
|
if tx.err != nil {
|
||||||
return tx.err
|
return tx.err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := tx.asCommandBuf()
|
nft.buffer.Reset()
|
||||||
|
err := tx.populateCommandBuf(nft.buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, nft.path, "--check", "-f", "-")
|
cmd := exec.CommandContext(ctx, nft.path, "--check", "-f", "-")
|
||||||
cmd.Stdin = buf
|
cmd.Stdin = nft.buffer
|
||||||
_, err = nft.exec.Run(cmd)
|
_, err = nft.exec.Run(cmd)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -298,7 +312,13 @@ func (nft *realNFTables) List(ctx context.Context, objectType string) ([]string,
|
|||||||
|
|
||||||
// ListRules is part of Interface
|
// ListRules is part of Interface
|
||||||
func (nft *realNFTables) ListRules(ctx context.Context, chain string) ([]*Rule, error) {
|
func (nft *realNFTables) ListRules(ctx context.Context, chain string) ([]*Rule, error) {
|
||||||
cmd := exec.CommandContext(ctx, nft.path, "--json", "list", "chain", string(nft.family), nft.table, chain)
|
// If no chain is given, return all rules from within the table.
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if chain == "" {
|
||||||
|
cmd = exec.CommandContext(ctx, nft.path, "--json", "list", "table", string(nft.family), nft.table)
|
||||||
|
} else {
|
||||||
|
cmd = exec.CommandContext(ctx, nft.path, "--json", "list", "chain", string(nft.family), nft.table, chain)
|
||||||
|
}
|
||||||
out, err := nft.exec.Run(cmd)
|
out, err := nft.exec.Run(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to run nft: %w", err)
|
return nil, fmt.Errorf("failed to run nft: %w", err)
|
||||||
@ -311,8 +331,12 @@ func (nft *realNFTables) ListRules(ctx context.Context, chain string) ([]*Rule,
|
|||||||
|
|
||||||
rules := make([]*Rule, 0, len(jsonRules))
|
rules := make([]*Rule, 0, len(jsonRules))
|
||||||
for _, jsonRule := range jsonRules {
|
for _, jsonRule := range jsonRules {
|
||||||
|
parentChain, ok := jsonVal[string](jsonRule, "chain")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected JSON output from nft (rule with no chain)")
|
||||||
|
}
|
||||||
rule := &Rule{
|
rule := &Rule{
|
||||||
Chain: chain,
|
Chain: parentChain,
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle is written as an integer in nft's output, but json.Unmarshal
|
// handle is written as an integer in nft's output, but json.Unmarshal
|
||||||
@ -404,7 +428,7 @@ func (nft *realNFTables) ListElements(ctx context.Context, objectType, name stri
|
|||||||
return elements, nil
|
return elements, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseElementValue parses a JSON element key/value, handling concatenations, and
|
// parseElementValue parses a JSON element key/value, handling concatenations, prefixes, and
|
||||||
// converting numeric or "verdict" values to strings.
|
// converting numeric or "verdict" values to strings.
|
||||||
func parseElementValue(json interface{}) ([]string, error) {
|
func parseElementValue(json interface{}) ([]string, error) {
|
||||||
// json can be:
|
// json can be:
|
||||||
@ -413,6 +437,14 @@ func parseElementValue(json interface{}) ([]string, error) {
|
|||||||
//
|
//
|
||||||
// - a single number, e.g. 80
|
// - a single number, e.g. 80
|
||||||
//
|
//
|
||||||
|
// - a prefix, expressed as an object:
|
||||||
|
// {
|
||||||
|
// "prefix": {
|
||||||
|
// "addr": "192.168.0.0",
|
||||||
|
// "len": 16,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
// - a concatenation, expressed as an object containing an array of simple
|
// - a concatenation, expressed as an object containing an array of simple
|
||||||
// values:
|
// values:
|
||||||
// {
|
// {
|
||||||
@ -452,6 +484,17 @@ func parseElementValue(json interface{}) ([]string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vals, nil
|
return vals, nil
|
||||||
|
} else if prefix, _ := jsonVal[map[string]interface{}](val, "prefix"); prefix != nil {
|
||||||
|
// For prefix-type elements, return the element in CIDR representation.
|
||||||
|
addr, ok := jsonVal[string](prefix, "addr")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("could not parse 'addr' value as string: %q", prefix)
|
||||||
|
}
|
||||||
|
length, ok := jsonVal[float64](prefix, "len")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("could not parse 'len' value as number: %q", prefix)
|
||||||
|
}
|
||||||
|
return []string{fmt.Sprintf("%s/%d", addr, int(length))}, nil
|
||||||
} else if len(val) == 1 {
|
} else if len(val) == 1 {
|
||||||
var verdict string
|
var verdict string
|
||||||
// We just checked that len(val) == 1, so this loop body will only
|
// We just checked that len(val) == 1, so this loop body will only
|
||||||
|
15
vendor/sigs.k8s.io/knftables/transaction.go
generated
vendored
15
vendor/sigs.k8s.io/knftables/transaction.go
generated
vendored
@ -19,7 +19,6 @@ package knftables
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Transaction represents an nftables transaction
|
// Transaction represents an nftables transaction
|
||||||
@ -48,17 +47,16 @@ const (
|
|||||||
flushVerb verb = "flush"
|
flushVerb verb = "flush"
|
||||||
)
|
)
|
||||||
|
|
||||||
// asCommandBuf returns the transaction as an io.Reader that outputs a series of nft commands
|
// populateCommandBuf populates the transaction as series of nft commands to the given bytes.Buffer.
|
||||||
func (tx *Transaction) asCommandBuf() (io.Reader, error) {
|
func (tx *Transaction) populateCommandBuf(buf *bytes.Buffer) error {
|
||||||
if tx.err != nil {
|
if tx.err != nil {
|
||||||
return nil, tx.err
|
return tx.err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
for _, op := range tx.operations {
|
for _, op := range tx.operations {
|
||||||
op.obj.writeOperation(op.verb, tx.nftContext, buf)
|
op.obj.writeOperation(op.verb, tx.nftContext, buf)
|
||||||
}
|
}
|
||||||
return buf, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the transaction as a string containing the nft commands; if there is
|
// String returns the transaction as a string containing the nft commands; if there is
|
||||||
@ -76,6 +74,11 @@ func (tx *Transaction) String() string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NumOperations returns the number of operations queued in the transaction.
|
||||||
|
func (tx *Transaction) NumOperations() int {
|
||||||
|
return len(tx.operations)
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *Transaction) operation(verb verb, obj Object) {
|
func (tx *Transaction) operation(verb verb, obj Object) {
|
||||||
if tx.err != nil {
|
if tx.err != nil {
|
||||||
return
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user